keepstore
keep-web
libarvados-perl
+ libpam-arvados
+ libpam-arvados-go
python-arvados-fuse
python-arvados-python-client
python-arvados-cwl-runner"
#
# SPDX-License-Identifier: AGPL-3.0
-. `dirname "$(readlink -f "$0")"`/run-library.sh
-. `dirname "$(readlink -f "$0")"`/libcloud-pin.sh
+. `dirname "$(readlink -f "$0")"`/run-library.sh || exit 1
+. `dirname "$(readlink -f "$0")"`/libcloud-pin.sh || exit 1
read -rd "\000" helpmessage <<EOF
$(basename $0): Build Arvados packages
perl Makefile.PL INSTALL_BASE=install >"$STDOUT_IF_DEBUG" && \
make install INSTALLDIRS=perl >"$STDOUT_IF_DEBUG" && \
- fpm_build install/lib/=/usr/share libarvados-perl \
+ fpm_build "$WORKSPACE/sdk/perl" install/lib/=/usr/share libarvados-perl \
dir "$(version_from_git)" install/man/=/usr/share/man \
"$WORKSPACE/apache-2.0.txt=/usr/share/doc/libarvados-perl/apache-2.0.txt" && \
mv --no-clobber libarvados-perl*.$FORMAT "$WORKSPACE/packages/$TARGET/"
cd "$SRC_BUILD_DIR"
PKG_VERSION=$(version_from_git)
cd $WORKSPACE/packages/$TARGET
- fpm_build $SRC_BUILD_DIR/=/usr/local/arvados/src arvados-src 'dir' "$PKG_VERSION" "--exclude=usr/local/arvados/src/.git" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=The Arvados source code" "--architecture=all"
+ fpm_build "$WORKSPACE" $SRC_BUILD_DIR/=/usr/local/arvados/src arvados-src 'dir' "$PKG_VERSION" "--exclude=usr/local/arvados/src/.git" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=The Arvados source code" "--architecture=all"
rm -rf "$SRC_BUILD_DIR"
fi
"Copy all data from one set of Keep servers to another"
package_go_binary tools/keep-exercise keep-exercise \
"Performance testing tool for Arvados Keep"
+package_go_so lib/pam pam_arvados.so libpam-arvados-go \
+ "Arvados PAM authentication module"
# The Python SDK - Should be built first because it's needed by others
fpm_build_virtualenv "arvados-python-client" "sdk/python"
__returnvar="$version"
}
-# Usage: package_go_binary services/foo arvados-foo "Compute foo to arbitrary precision"
+# Usage: package_go_binary services/foo arvados-foo "Compute foo to arbitrary precision" [apache-2.0.txt]
package_go_binary() {
local src_path="$1"; shift
local prog="$1"; shift
fi
switches+=("$WORKSPACE/${license_file}=/usr/share/doc/$prog/${license_file}")
- fpm_build "$GOPATH/bin/${basename}=/usr/bin/${prog}" "${prog}" dir "${go_package_version}" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=${description}" "${switches[@]}"
+ fpm_build "${WORKSPACE}/${src_path}" "$GOPATH/bin/${basename}=/usr/bin/${prog}" "${prog}" dir "${go_package_version}" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=${description}" "${switches[@]}"
+}
+
+# Usage: package_go_so lib/foo arvados_foo.so arvados-foo "Arvados foo library"
+package_go_so() {
+ local src_path="$1"; shift
+ local sofile="$1"; shift
+ local pkg="$1"; shift
+ local description="$1"; shift
+
+ debug_echo "package_go_so $src_path as $pkg"
+
+ calculate_go_package_version go_package_version $src_path
+ cd $WORKSPACE/packages/$TARGET
+ test_package_presence $pkg $go_package_version go || return 1
+ cd $WORKSPACE/$src_path
+ go build -buildmode=c-shared -o ${GOPATH}/bin/${sofile}
+ cd $WORKSPACE/packages/$TARGET
+ local -a fpmargs=(
+ "--url=https://arvados.org"
+ "--license=Apache License, Version 2.0"
+ "--description=${description}"
+ "$WORKSPACE/apache-2.0.txt=/usr/share/doc/$pkg/apache-2.0.txt"
+ )
+ if [[ -e "$WORKSPACE/$src_path/pam-configs-arvados" ]]; then
+ fpmargs+=("$WORKSPACE/$src_path/pam-configs-arvados=/usr/share/pam-configs/arvados-go")
+ fi
+ if [[ -e "$WORKSPACE/$src_path/README" ]]; then
+ fpmargs+=("$WORKSPACE/$src_path/README=/usr/share/doc/$pkg/README")
+ fi
+ fpm_build "${WORKSPACE}/${src_path}" "$GOPATH/bin/${sofile}=/usr/lib/${sofile}" "${pkg}" dir "${go_package_version}" "${fpmargs[@]}"
}
default_iteration() {
for exclude in ${exclude_list[@]}; do
switches+=(-x "$exclude_root/$exclude")
done
- fpm_build "${pos_args[@]}" "${switches[@]}" \
+ fpm_build "${srcdir}" "${pos_args[@]}" "${switches[@]}" \
-x "$exclude_root/vendor/cache-*" \
-x "$exclude_root/vendor/bundle" "$@" "$license_arg"
rm -rf "$scripts_dir"
# Build packages for everything
fpm_build () {
+ # Source dir where fpm-info.sh (if any) will be found.
+ SRC_DIR=$1
+ shift
# The package source. Depending on the source type, this can be a
# path, or the name of the package in an upstream repository (e.g.,
# pip).
declare -a build_depends=()
declare -a fpm_depends=()
declare -a fpm_exclude=()
- declare -a fpm_dirs=(
- # source dir part of 'dir' package ("/source=/dest" => "/source"):
- "${PACKAGE%%=/*}")
- for pkgdir in "${fpm_dirs[@]}"; do
- fpminfo="$pkgdir/fpm-info.sh"
- if [[ -e "$fpminfo" ]]; then
- debug_echo "Loading fpm overrides from $fpminfo"
- source "$fpminfo"
- break
- fi
- done
+ if [[ ! -d "$SRC_DIR" ]]; then
+ echo >&2 "BUG: looking in wrong dir for fpm-info.sh: $pkgdir"
+ exit 1
+ fi
+ fpminfo="${SRC_DIR}/fpm-info.sh"
+ if [[ -e "$fpminfo" ]]; then
+ debug_echo "Loading fpm overrides from $fpminfo"
+ source "$fpminfo"
+ fi
for pkg in "${build_depends[@]}"; do
if [[ $TARGET =~ debian|ubuntu ]]; then
pkg_deb=$(ls "$WORKSPACE/packages/$TARGET/$pkg_"*.deb | sort -rg | awk 'NR==1')
- admin/index.html.textile.liquid
- Users and Groups:
- admin/user-management.html.textile.liquid
- - admin/reassign-ownership.html.textile.liquid
- admin/user-management-cli.html.textile.liquid
+ - admin/reassign-ownership.html.textile.liquid
+ - admin/link-accounts.html.textile.liquid
- admin/group-management.html.textile.liquid
- admin/federation.html.textile.liquid
- admin/merge-remote-account.html.textile.liquid
--- /dev/null
+---
+layout: default
+navsection: admin
+title: "Link user accounts"
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+If a user needs to log in to Arvados with a upstream account or provider, they may end up with two Arvados user accounts. If the user still has the ability to log in with the old account, they can use the "self-serve account linking":{{site.baseurl}}/user/topics/link-accounts.html feature of workbench. However, if the user does not have the ability to log in with both upstream accounts, the admin can also link the accounts using the command line.
+
+h3. Step 1: Determine user uuids
+
+User uuids can be determined by browsing workbench or using @arv user list@ at the command line.
+
+Account linking works by recording in the database that a log in to the "old" account should redirected and treated as a login to the "new" account.
+
+The "old" account is the Arvados account that will be redirected.
+
+The "new" account is the user that the "old" account is redirected to. As part of account linking any Arvados records owned by the "old" account is also transferred to the "new" account.
+
+Counter-intuitively, if you do not want the account uuid of the user to change, the "new" account should be the pre-existing account, and the "old" account should be the redundant second account that was more recently created. This means "old" and "new" are opposite from their expected chronological meaning. In this case, the use of "old" and "new" reflect the direction of transfer of ownership -- the login was associated with the "old" user account, but will be associated with the "new" user account.
+
+In the example below, @zzzzz-tpzed-3kz0nwtjehhl0u4@ is the "old" account (the pre-existing account we want to keep) and @zzzzz-tpzed-fr97h9t4m5jffxs@ is the "new" account (the redundant account we want to merge into the existing account).
+
+h3. Step 2: Create a project
+
+Create a project owned by the "new" account that will hold any data owned by the "old" account.
+
+<pre>
+$ arv --format=uuid group create --group '{"group_class": "project", "name": "Data from old user", "owner_uuid": "zzzzz-tpzed-fr97h9t4m5jffxs"}'
+zzzzz-j7d0g-mczqiguhil13083
+</pre>
+
+h3. Step 3: Merge "old" user to "new" user
+
+The @user merge@ method redirects login and reassigns data from the "old" account to the "new" account.
+
+<pre>
+$ arv user merge --redirect-to-new-user \
+ --old-user-uuid=zzzzz-tpzed-3kz0nwtjehhl0u4 \
+ --new-user-uuid=zzzzz-tpzed-fr97h9t4m5jffxs \
+ --new-owner-uuid=zzzzz-j7d0g-mczqiguhil13083 \
+</pre>
+
+Note that authorization credentials (API tokens, ssh keys) are also transferred to the "new" account, so credentials used to access the "old" account work with the "new" account.
table(table table-bordered table-condensed).
|_. Argument |_. Type |_. Description |_. Location |_. Example |
{background:#ccffcc}.|uuid|string|The UUID of the User in question.|path||
+
+h3. merge
+
+Transfer ownership of data from the "old" user account to the "new" user account. When @redirect_to_new_user@ is @true@ this also causes logins to the "old" account to be redirected to the "new" account. The "old" user account that was redirected becomes invisible in user listings.
+
+See "Merge user accounts":{{site.baseurl}}/admin/link-accounts.html , "Reassign user data ownership":{{site.baseurl}}/admin/reassign-ownership.html and "Linking alternate login accounts":{{site.baseurl}}/user/topics/link-accounts.html for examples of how this method is used.
+
+Must supply either @new_user_token@ (the currently authorized user will be the "old" user), or both @new_user_uuid@ and @old_user_uuid@ (the currently authorized user must be an admin).
+
+Arguments:
+
+table(table table-bordered table-condensed).
+|_. Argument |_. Type |_. Description |_. Location |_. Example |
+|new_user_token|string|A valid token for the "new" user|query||
+|new_user_uuid|uuid|The uuid of the "new" account|query||
+|old_user_uuid|uuid|The uuid of the "old" account|query||
+|new_owner_uuid|uuid|The uuid of a project to which objects owned by the "old" user will be reassigned.|query||
+|redirect_to_new_user|boolean|If true, also redirect login and reassign authorization credentials from "old" user to the "new" user|query||
# This configures the public https port that clients will actually connect to,
# the request is reverse proxied to the upstream 'controller'
- listen *:443 ssl;
- server_name <span class="userinput">xxxxx.example.com</span>;
+ listen 443 ssl;
+ server_name <span class="userinput">ClusterID.example.com</span>;
- ssl on;
ssl_certificate <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
server 127.0.0.1:<span class="userinput">9001</span>;
}
server {
- listen *:443 ssl;
+ listen 443 ssl;
server_name git.<span class="userinput">ClusterID.example.com</span>;
proxy_connect_timeout 90s;
proxy_read_timeout 300s;
- ssl on;
ssl_certificate <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
}
server {
- listen *:443 ssl;
+ listen 443 ssl;
server_name <span class="userinput">download.ClusterID.example.com</span>
<span class="userinput">collections.ClusterID.example.com</span>
<span class="userinput">*.collections.ClusterID.example.com</span>
}
server {
- listen *:443 ssl;
+ listen 443 ssl;
server_name <span class="userinput">keep.ClusterID.example.com</span>;
proxy_connect_timeout 90s;
proxy_http_version 1.1;
proxy_request_buffering off;
- ssl on;
ssl_certificate <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
For additional shell nodes with @shell-in-a-box@, add @location@ and @upstream@ sections as needed.
-{% assign arvados_component = 'shellinabox libpam-arvados' %}
+{% assign arvados_component = 'shellinabox libpam-arvados-go' %}
{% include 'install_packages' %}
Use a text editor to create a new file @/etc/pam.d/shellinabox@ with the following configuration. Options that need attention are marked in <span class="userinput">red</span>.
<notextile><pre>
-# This example is a stock debian "login" file with libpam_arvados
-# replacing pam_unix, and the "noprompt" option in use. It can be
-# installed as /etc/pam.d/shellinabox .
+# This example is a stock debian "login" file with pam_arvados
+# replacing pam_unix. It can be installed as /etc/pam.d/shellinabox .
auth optional pam_faildelay.so delay=3000000
auth [success=ok new_authtok_reqd=ok ignore=ignore user_unknown=bad default=die] pam_securetty.so
session required pam_env.so readenv=1
session required pam_env.so readenv=1 envfile=/etc/default/locale
-auth [success=1 default=ignore] pam_python.so /usr/lib/security/libpam_arvados.py <span class="userinput">ClusterID.example.com</span> <span class="userinput">shell.ClusterID.example.com</span> noprompt
+auth [success=1 default=ignore] /usr/lib/pam_arvados.so <span class="userinput">ClusterID.example.com</span> <span class="userinput">shell.ClusterID.example.com</span>
auth requisite pam_deny.so
auth required pam_permit.so
}
server {
- listen *:443 ssl;
+ listen 443 ssl;
server_name workbench.<span class="userinput">ClusterID.example.com</span>;
- ssl on;
ssl_certificate <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
}
server {
- listen *:443 ssl;
+ listen 443 ssl;
server_name workbench2.<span class="userinput">ClusterID.example.com</span>;
- ssl on;
ssl_certificate <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
}
server {
- listen *:443 ssl;
+ listen 443 ssl;
server_name ws.<span class="userinput">ClusterID.example.com</span>;
proxy_connect_timeout 90s;
// non-nil, true, nil -- if the token is valid
func (h *Handler) validateAPItoken(req *http.Request, token string) (*CurrentUser, bool, error) {
user := CurrentUser{Authorization: arvados.APIClientAuthorization{APIToken: token}}
- db, err := h.db(req)
+ db, err := h.db(req.Context())
if err != nil {
ctxlog.FromContext(req.Context()).WithError(err).Debugf("validateAPItoken(%s): database error", token)
return nil, false, err
}
func (h *Handler) createAPItoken(req *http.Request, userUUID string, scopes []string) (*arvados.APIClientAuthorization, error) {
- db, err := h.db(req)
+ db, err := h.db(req.Context())
if err != nil {
return nil, err
}
func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
srv := httpserver.Server{Addr: ":"}
- srv.Handler = router.New(backend)
+ srv.Handler = router.New(backend, nil)
c.Check(srv.Start(), check.IsNil)
s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
Scheme: "http",
"time"
"git.arvados.org/arvados.git/lib/controller/federation"
+ "git.arvados.org/arvados.git/lib/controller/localdb"
"git.arvados.org/arvados.git/lib/controller/railsproxy"
"git.arvados.org/arvados.git/lib/controller/router"
"git.arvados.org/arvados.git/sdk/go/arvados"
+ "git.arvados.org/arvados.git/sdk/go/ctxlog"
"git.arvados.org/arvados.git/sdk/go/health"
"git.arvados.org/arvados.git/sdk/go/httpserver"
_ "github.com/lib/pq"
func (h *Handler) CheckHealth() error {
h.setupOnce.Do(h.setup)
- _, _, err := railsproxy.FindRailsAPI(h.Cluster)
+ _, err := h.db(context.TODO())
+ if err != nil {
+ return err
+ }
+ _, _, err = railsproxy.FindRailsAPI(h.Cluster)
return err
}
mux.Handle("/_health/", &health.Handler{
Token: h.Cluster.ManagementToken,
Prefix: "/_health/",
- Routes: health.Routes{"ping": func() error { _, err := h.db(&http.Request{}); return err }},
+ Routes: health.Routes{"ping": func() error { _, err := h.db(context.TODO()); return err }},
})
- rtr := router.New(federation.New(h.Cluster))
+ rtr := router.New(federation.New(h.Cluster), localdb.WrapCallsInTransactions(h.db))
mux.Handle("/arvados/v1/config", rtr)
mux.Handle("/"+arvados.EndpointUserAuthenticate.Path, rtr)
var errDBConnection = errors.New("database connection error")
-func (h *Handler) db(req *http.Request) (*sql.DB, error) {
+func (h *Handler) db(ctx context.Context) (*sql.DB, error) {
h.pgdbMtx.Lock()
defer h.pgdbMtx.Unlock()
if h.pgdb != nil {
db, err := sql.Open("postgres", h.Cluster.PostgreSQL.Connection.String())
if err != nil {
- httpserver.Logger(req).WithError(err).Error("postgresql connect failed")
+ ctxlog.FromContext(ctx).WithError(err).Error("postgresql connect failed")
return nil, errDBConnection
}
if p := h.Cluster.PostgreSQL.ConnectionPool; p > 0 {
db.SetMaxOpenConns(p)
}
if err := db.Ping(); err != nil {
- httpserver.Logger(req).WithError(err).Error("postgresql connect succeeded but ping failed")
+ ctxlog.FromContext(ctx).WithError(err).Error("postgresql connect scuceeded but ping failed")
return nil, errDBConnection
}
h.pgdb = db
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "sync"
+
+ "git.arvados.org/arvados.git/lib/controller/router"
+ "git.arvados.org/arvados.git/sdk/go/ctxlog"
+)
+
+// WrapCallsInTransactions returns a call wrapper (suitable for
+// assigning to router.router.WrapCalls) that starts a new transaction
+// for each API call, and commits only if the call succeeds.
+//
+// The wrapper calls getdb() to get a database handle before each API
+// call.
+func WrapCallsInTransactions(getdb func(context.Context) (*sql.DB, error)) func(router.RoutableFunc) router.RoutableFunc {
+ return func(origFunc router.RoutableFunc) router.RoutableFunc {
+ return func(ctx context.Context, opts interface{}) (_ interface{}, err error) {
+ ctx, finishtx := starttx(ctx, getdb)
+ defer finishtx(&err)
+ return origFunc(ctx, opts)
+ }
+ }
+}
+
+// ContextWithTransaction returns a child context in which the given
+// transaction will be used by any localdb API call that needs one.
+// The caller is responsible for calling Commit or Rollback on tx.
+func ContextWithTransaction(ctx context.Context, tx *sql.Tx) context.Context {
+ txn := &transaction{tx: tx}
+ txn.setup.Do(func() {})
+ return context.WithValue(ctx, contextKeyTransaction, txn)
+}
+
+type contextKeyT string
+
+var contextKeyTransaction = contextKeyT("transaction")
+
+type transaction struct {
+ tx *sql.Tx
+ err error
+ getdb func(context.Context) (*sql.DB, error)
+ setup sync.Once
+}
+
+type transactionFinishFunc func(*error)
+
+// starttx returns a new child context that can be used with
+// currenttx(). It does not open a database transaction until the
+// first call to currenttx().
+//
+// The caller must eventually call the returned finishtx() func to
+// commit or rollback the transaction, if any.
+//
+// func example(ctx context.Context) (err error) {
+// ctx, finishtx := starttx(ctx, dber)
+// defer finishtx(&err)
+// // ...
+// tx, err := currenttx(ctx)
+// if err != nil {
+// return fmt.Errorf("example: %s", err)
+// }
+// return tx.ExecContext(...)
+// }
+//
+// If *err is nil, finishtx() commits the transaction and assigns any
+// resulting error to *err.
+//
+// If *err is non-nil, finishtx() rolls back the transaction, and
+// does not modify *err.
+func starttx(ctx context.Context, getdb func(context.Context) (*sql.DB, error)) (context.Context, transactionFinishFunc) {
+ txn := &transaction{getdb: getdb}
+ return context.WithValue(ctx, contextKeyTransaction, txn), func(err *error) {
+ txn.setup.Do(func() {
+ // Using (*sync.Once)Do() prevents a future
+ // call to currenttx() from opening a
+ // transaction which would never get committed
+ // or rolled back. If currenttx() hasn't been
+ // called before now, future calls will return
+ // this error.
+ txn.err = errors.New("refusing to start a transaction after wrapped function already returned")
+ })
+ if txn.tx == nil {
+ // we never [successfully] started a transaction
+ return
+ }
+ if *err != nil {
+ ctxlog.FromContext(ctx).Debug("rollback")
+ txn.tx.Rollback()
+ return
+ }
+ *err = txn.tx.Commit()
+ }
+}
+
+func currenttx(ctx context.Context) (*sql.Tx, error) {
+ txn, ok := ctx.Value(contextKeyTransaction).(*transaction)
+ if !ok {
+ return nil, errors.New("bug: there is no transaction in this context")
+ }
+ txn.setup.Do(func() {
+ if db, err := txn.getdb(ctx); err != nil {
+ txn.err = err
+ } else {
+ txn.tx, txn.err = db.Begin()
+ }
+ })
+ return txn.tx, txn.err
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+ "context"
+ "database/sql"
+ "sync"
+ "sync/atomic"
+
+ "git.arvados.org/arvados.git/lib/config"
+ "git.arvados.org/arvados.git/sdk/go/arvados"
+ "git.arvados.org/arvados.git/sdk/go/ctxlog"
+ _ "github.com/lib/pq"
+ check "gopkg.in/check.v1"
+)
+
+// testdb returns a DB connection for the given cluster config.
+func testdb(c *check.C, cluster *arvados.Cluster) *sql.DB {
+ db, err := sql.Open("postgres", cluster.PostgreSQL.Connection.String())
+ c.Assert(err, check.IsNil)
+ return db
+}
+
+// testctx returns a context suitable for running a test case in a new
+// transaction, and a rollback func which the caller should call after
+// the test.
+func testctx(c *check.C, db *sql.DB) (ctx context.Context, rollback func()) {
+ tx, err := db.Begin()
+ c.Assert(err, check.IsNil)
+ return ContextWithTransaction(context.Background(), tx), func() {
+ c.Check(tx.Rollback(), check.IsNil)
+ }
+}
+
+var _ = check.Suite(&DatabaseSuite{})
+
+type DatabaseSuite struct{}
+
+func (*DatabaseSuite) TestTransactionContext(c *check.C) {
+ cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
+ c.Assert(err, check.IsNil)
+ cluster, err := cfg.GetCluster("")
+ c.Assert(err, check.IsNil)
+
+ var getterCalled int64
+ getter := func(context.Context) (*sql.DB, error) {
+ atomic.AddInt64(&getterCalled, 1)
+ return testdb(c, cluster), nil
+ }
+ wrapper := WrapCallsInTransactions(getter)
+ wrappedFunc := wrapper(func(ctx context.Context, opts interface{}) (interface{}, error) {
+ txes := make([]*sql.Tx, 20)
+ var wg sync.WaitGroup
+ for i := range txes {
+ i := i
+ wg.Add(1)
+ go func() {
+ // Concurrent calls to currenttx(),
+ // with different children of the same
+ // parent context, will all return the
+ // same transaction.
+ defer wg.Done()
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ tx, err := currenttx(ctx)
+ c.Check(err, check.IsNil)
+ txes[i] = tx
+ }()
+ }
+ wg.Wait()
+ for i := range txes[1:] {
+ c.Check(txes[i], check.Equals, txes[i+1])
+ }
+ return true, nil
+ })
+
+ ok, err := wrappedFunc(context.Background(), "blah")
+ c.Check(ok, check.Equals, true)
+ c.Check(err, check.IsNil)
+ c.Check(getterCalled, check.Equals, int64(1))
+
+ // When a wrapped func returns without calling currenttx(),
+ // calling currenttx() later shouldn't start a new
+ // transaction.
+ var savedctx context.Context
+ ok, err = wrapper(func(ctx context.Context, opts interface{}) (interface{}, error) {
+ savedctx = ctx
+ return true, nil
+ })(context.Background(), "blah")
+ c.Check(ok, check.Equals, true)
+ c.Check(err, check.IsNil)
+ tx, err := currenttx(savedctx)
+ c.Check(tx, check.IsNil)
+ c.Check(err, check.NotNil)
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+ "io"
+ "net"
+ "strings"
+
+ "git.arvados.org/arvados.git/sdk/go/arvados"
+ check "gopkg.in/check.v1"
+)
+
+type pgproxy struct {
+ net.Listener
+}
+
+// newPgProxy sets up a TCP proxy, listening on all interfaces, that
+// forwards all connections to the cluster's PostgreSQL server. This
+// allows the caller to run a docker container that can connect to a
+// postgresql instance that listens on the test host's loopback
+// interface.
+//
+// Caller is responsible for calling Close() on the returned pgproxy.
+func newPgProxy(c *check.C, cluster *arvados.Cluster) *pgproxy {
+ host := cluster.PostgreSQL.Connection["host"]
+ if host == "" {
+ host = "localhost"
+ }
+ port := cluster.PostgreSQL.Connection["port"]
+ if port == "" {
+ port = "5432"
+ }
+ target := net.JoinHostPort(host, port)
+
+ ln, err := net.Listen("tcp", ":")
+ c.Assert(err, check.IsNil)
+ go func() {
+ for {
+ downstream, err := ln.Accept()
+ if err != nil && strings.Contains(err.Error(), "use of closed network connection") {
+ return
+ }
+ c.Assert(err, check.IsNil)
+ go func() {
+ c.Logf("pgproxy accepted connection from %s", downstream.RemoteAddr().String())
+ defer downstream.Close()
+ upstream, err := net.Dial("tcp", target)
+ if err != nil {
+ c.Logf("net.Dial(%q): %s", target, err)
+ return
+ }
+ defer upstream.Close()
+ go io.Copy(downstream, upstream)
+ io.Copy(upstream, downstream)
+ }()
+ }
+ }()
+ c.Logf("pgproxy listening at %s", ln.Addr().String())
+ return &pgproxy{Listener: ln}
+}
+
+func (proxy *pgproxy) Port() string {
+ _, port, _ := net.SplitHostPort(proxy.Addr().String())
+ return port
+}
import (
"context"
+ "database/sql"
+ "encoding/json"
"errors"
+ "fmt"
"net/http"
"net/url"
+ "strings"
"git.arvados.org/arvados.git/lib/controller/rpc"
"git.arvados.org/arvados.git/sdk/go/arvados"
return arvados.LogoutResponse{RedirectLocation: target}, nil
}
-func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (arvados.APIClientAuthorization, error) {
+func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (resp arvados.APIClientAuthorization, err error) {
ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{rootToken}})
- resp, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+ newsession, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
// Send a fake ReturnTo value instead of the caller's
// opts.ReturnTo. We won't follow the resulting
// redirect target anyway.
AuthInfo: authinfo,
})
if err != nil {
- return arvados.APIClientAuthorization{}, err
+ return
}
- target, err := url.Parse(resp.RedirectLocation)
+ target, err := url.Parse(newsession.RedirectLocation)
if err != nil {
- return arvados.APIClientAuthorization{}, err
+ return
}
token := target.Query().Get("api_token")
- return conn.APIClientAuthorizationCurrent(auth.NewContext(ctx, auth.NewCredentials(token)), arvados.GetOptions{})
+ tx, err := currenttx(ctx)
+ if err != nil {
+ return
+ }
+ tokensecret := token
+ if strings.Contains(token, "/") {
+ tokenparts := strings.Split(token, "/")
+ if len(tokenparts) >= 3 {
+ tokensecret = tokenparts[2]
+ }
+ }
+ var exp sql.NullString
+ var scopes []byte
+ err = tx.QueryRowContext(ctx, "select uuid, api_token, expires_at, scopes from api_client_authorizations where api_token=$1", tokensecret).Scan(&resp.UUID, &resp.APIToken, &exp, &scopes)
+ if err != nil {
+ return
+ }
+ resp.ExpiresAt = exp.String
+ if len(scopes) > 0 {
+ err = json.Unmarshal(scopes, &resp.Scopes)
+ if err != nil {
+ return resp, fmt.Errorf("unmarshal scopes: %s", err)
+ }
+ }
+ return
}
if !haveDocker() {
c.Skip("skipping docker test because docker is not available")
}
+ pgproxy := newPgProxy(c, s.cluster)
+ defer pgproxy.Close()
+
cmd := exec.Command("bash", "login_ldap_docker_test.sh")
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
- cmd.Env = append(os.Environ(), "config_method=pam")
+ cmd.Env = append(os.Environ(), "config_method=pam", "pgport="+pgproxy.Port())
err := cmd.Run()
c.Check(err, check.IsNil)
}
if !haveDocker() {
c.Skip("skipping docker test because docker is not available")
}
+ pgproxy := newPgProxy(c, s.cluster)
+ defer pgproxy.Close()
+
cmd := exec.Command("bash", "login_ldap_docker_test.sh")
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
- cmd.Env = append(os.Environ(), "config_method=ldap")
+ cmd.Env = append(os.Environ(), "config_method=ldap", "pgport="+pgproxy.Port())
err := cmd.Run()
c.Check(err, check.IsNil)
}
#!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
# This script demonstrates using LDAP for Arvados user authentication.
#
# It configures arvados controller in a docker container, optionally
Connection:
client_encoding: utf8
host: ${hostname}
+ port: "${pgport}"
dbname: arvados_test
user: arvados
password: insecure_arvados_test
import (
"context"
+ "database/sql"
"encoding/json"
"net"
"net/http"
cluster *arvados.Cluster
ctrl *ldapLoginController
ldap *godap.LDAPServer // fake ldap server that accepts auth goodusername/goodpassword
+ db *sql.DB
+
+ // transaction context
+ ctx context.Context
+ rollback func()
}
func (s *LDAPSuite) TearDownSuite(c *check.C) {
Cluster: s.cluster,
RailsProxy: railsproxy.NewConn(s.cluster),
}
+ s.db = testdb(c, s.cluster)
+}
+
+func (s *LDAPSuite) SetUpTest(c *check.C) {
+ s.ctx, s.rollback = testctx(c, s.db)
+}
+
+func (s *LDAPSuite) TearDownTest(c *check.C) {
+ s.rollback()
}
func (s *LDAPSuite) TestLoginSuccess(c *check.C) {
- resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+ conn := NewConn(s.cluster)
+ conn.loginController = s.ctrl
+ resp, err := conn.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
Username: "goodusername",
Password: "goodpassword",
})
c.Check(resp.UUID, check.Matches, `zzzzz-gj3su-.*`)
c.Check(resp.Scopes, check.DeepEquals, []string{"all"})
- ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{"v2/" + resp.UUID + "/" + resp.APIToken}})
+ ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{"v2/" + resp.UUID + "/" + resp.APIToken}})
user, err := railsproxy.NewConn(s.cluster).UserGetCurrent(ctx, arvados.GetOptions{})
c.Check(err, check.IsNil)
c.Check(user.Email, check.Equals, "goodusername@example.com")
func (s *LDAPSuite) TestLoginFailure(c *check.C) {
// search returns no results
s.cluster.Login.LDAP.SearchBase = "dc=example,dc=invalid"
- resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+ resp, err := s.ctrl.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
Username: "goodusername",
Password: "goodpassword",
})
// search returns result, but auth fails
s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
- resp, err = s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+ resp, err = s.ctrl.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
Username: "badusername",
Password: "badpassword",
})
type OIDCLoginSuite struct {
cluster *arvados.Cluster
- ctx context.Context
localdb *Conn
railsSpy *arvadostest.Proxy
fakeIssuer *httptest.Server
)
type router struct {
- mux *mux.Router
- fed arvados.API
+ mux *mux.Router
+ backend arvados.API
+ wrapCalls func(RoutableFunc) RoutableFunc
}
-func New(fed arvados.API) *router {
+// New returns a new router (which implements the http.Handler
+// interface) that serves requests by calling Arvados API methods on
+// the given backend.
+//
+// If wrapCalls is not nil, it is called once for each API method, and
+// the returned method is used in its place. This can be used to
+// install hooks before and after each API call and alter responses;
+// see localdb.WrapCallsInTransaction for an example.
+func New(backend arvados.API, wrapCalls func(RoutableFunc) RoutableFunc) *router {
rtr := &router{
- mux: mux.NewRouter(),
- fed: fed,
+ mux: mux.NewRouter(),
+ backend: backend,
+ wrapCalls: wrapCalls,
}
rtr.addRoutes()
return rtr
}
-type routableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
+type RoutableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
func (rtr *router) addRoutes() {
for _, route := range []struct {
endpoint arvados.APIEndpoint
defaultOpts func() interface{}
- exec routableFunc
+ exec RoutableFunc
}{
{
arvados.EndpointConfigGet,
func() interface{} { return &struct{}{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ConfigGet(ctx)
+ return rtr.backend.ConfigGet(ctx)
},
},
{
arvados.EndpointLogin,
func() interface{} { return &arvados.LoginOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.Login(ctx, *opts.(*arvados.LoginOptions))
+ return rtr.backend.Login(ctx, *opts.(*arvados.LoginOptions))
},
},
{
arvados.EndpointLogout,
func() interface{} { return &arvados.LogoutOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.Logout(ctx, *opts.(*arvados.LogoutOptions))
+ return rtr.backend.Logout(ctx, *opts.(*arvados.LogoutOptions))
},
},
{
arvados.EndpointCollectionCreate,
func() interface{} { return &arvados.CreateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
+ return rtr.backend.CollectionCreate(ctx, *opts.(*arvados.CreateOptions))
},
},
{
arvados.EndpointCollectionUpdate,
func() interface{} { return &arvados.UpdateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ return rtr.backend.CollectionUpdate(ctx, *opts.(*arvados.UpdateOptions))
},
},
{
arvados.EndpointCollectionGet,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionGet(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.CollectionGet(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointCollectionList,
func() interface{} { return &arvados.ListOptions{Limit: -1} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionList(ctx, *opts.(*arvados.ListOptions))
+ return rtr.backend.CollectionList(ctx, *opts.(*arvados.ListOptions))
},
},
{
arvados.EndpointCollectionProvenance,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.CollectionProvenance(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointCollectionUsedBy,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.CollectionUsedBy(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointCollectionDelete,
func() interface{} { return &arvados.DeleteOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
+ return rtr.backend.CollectionDelete(ctx, *opts.(*arvados.DeleteOptions))
},
},
{
arvados.EndpointCollectionTrash,
func() interface{} { return &arvados.DeleteOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
+ return rtr.backend.CollectionTrash(ctx, *opts.(*arvados.DeleteOptions))
},
},
{
arvados.EndpointCollectionUntrash,
func() interface{} { return &arvados.UntrashOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
+ return rtr.backend.CollectionUntrash(ctx, *opts.(*arvados.UntrashOptions))
},
},
{
arvados.EndpointContainerCreate,
func() interface{} { return &arvados.CreateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
+ return rtr.backend.ContainerCreate(ctx, *opts.(*arvados.CreateOptions))
},
},
{
arvados.EndpointContainerUpdate,
func() interface{} { return &arvados.UpdateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ return rtr.backend.ContainerUpdate(ctx, *opts.(*arvados.UpdateOptions))
},
},
{
arvados.EndpointContainerGet,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerGet(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.ContainerGet(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointContainerList,
func() interface{} { return &arvados.ListOptions{Limit: -1} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerList(ctx, *opts.(*arvados.ListOptions))
+ return rtr.backend.ContainerList(ctx, *opts.(*arvados.ListOptions))
},
},
{
arvados.EndpointContainerDelete,
func() interface{} { return &arvados.DeleteOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
+ return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
},
},
{
return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
},
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerLock(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.ContainerLock(ctx, *opts.(*arvados.GetOptions))
},
},
{
return &arvados.GetOptions{Select: []string{"uuid", "state", "priority", "auth_uuid", "locked_by_uuid"}}
},
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointSpecimenCreate,
func() interface{} { return &arvados.CreateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
+ return rtr.backend.SpecimenCreate(ctx, *opts.(*arvados.CreateOptions))
},
},
{
arvados.EndpointSpecimenUpdate,
func() interface{} { return &arvados.UpdateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ return rtr.backend.SpecimenUpdate(ctx, *opts.(*arvados.UpdateOptions))
},
},
{
arvados.EndpointSpecimenGet,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.SpecimenGet(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointSpecimenList,
func() interface{} { return &arvados.ListOptions{Limit: -1} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.SpecimenList(ctx, *opts.(*arvados.ListOptions))
+ return rtr.backend.SpecimenList(ctx, *opts.(*arvados.ListOptions))
},
},
{
arvados.EndpointSpecimenDelete,
func() interface{} { return &arvados.DeleteOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
+ return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
},
},
{
arvados.EndpointUserCreate,
func() interface{} { return &arvados.CreateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserCreate(ctx, *opts.(*arvados.CreateOptions))
+ return rtr.backend.UserCreate(ctx, *opts.(*arvados.CreateOptions))
},
},
{
arvados.EndpointUserMerge,
func() interface{} { return &arvados.UserMergeOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
+ return rtr.backend.UserMerge(ctx, *opts.(*arvados.UserMergeOptions))
},
},
{
arvados.EndpointUserActivate,
func() interface{} { return &arvados.UserActivateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
+ return rtr.backend.UserActivate(ctx, *opts.(*arvados.UserActivateOptions))
},
},
{
arvados.EndpointUserSetup,
func() interface{} { return &arvados.UserSetupOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
+ return rtr.backend.UserSetup(ctx, *opts.(*arvados.UserSetupOptions))
},
},
{
arvados.EndpointUserUnsetup,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.UserUnsetup(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointUserGetCurrent,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.UserGetCurrent(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointUserGetSystem,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.UserGetSystem(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointUserGet,
func() interface{} { return &arvados.GetOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserGet(ctx, *opts.(*arvados.GetOptions))
+ return rtr.backend.UserGet(ctx, *opts.(*arvados.GetOptions))
},
},
{
arvados.EndpointUserUpdateUUID,
func() interface{} { return &arvados.UpdateUUIDOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
+ return rtr.backend.UserUpdateUUID(ctx, *opts.(*arvados.UpdateUUIDOptions))
},
},
{
arvados.EndpointUserUpdate,
func() interface{} { return &arvados.UpdateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
+ return rtr.backend.UserUpdate(ctx, *opts.(*arvados.UpdateOptions))
},
},
{
arvados.EndpointUserList,
func() interface{} { return &arvados.ListOptions{Limit: -1} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserList(ctx, *opts.(*arvados.ListOptions))
+ return rtr.backend.UserList(ctx, *opts.(*arvados.ListOptions))
},
},
{
arvados.EndpointUserBatchUpdate,
func() interface{} { return &arvados.UserBatchUpdateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
+ return rtr.backend.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
},
},
{
arvados.EndpointUserDelete,
func() interface{} { return &arvados.DeleteOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
+ return rtr.backend.UserDelete(ctx, *opts.(*arvados.DeleteOptions))
},
},
{
arvados.EndpointUserAuthenticate,
func() interface{} { return &arvados.UserAuthenticateOptions{} },
func(ctx context.Context, opts interface{}) (interface{}, error) {
- return rtr.fed.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
+ return rtr.backend.UserAuthenticate(ctx, *opts.(*arvados.UserAuthenticateOptions))
},
},
} {
- rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
+ exec := route.exec
+ if rtr.wrapCalls != nil {
+ exec = rtr.wrapCalls(exec)
+ }
+ rtr.addRoute(route.endpoint, route.defaultOpts, exec)
}
rtr.mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
httpserver.Errors(w, []string{"API endpoint not found"}, http.StatusNotFound)
"GET": "HEAD", // Accept HEAD at any GET route
}
-func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec routableFunc) {
+func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() interface{}, exec RoutableFunc) {
methods := []string{endpoint.Method}
if alt, ok := altMethod[endpoint.Method]; ok {
methods = append(methods, alt)
func (s *RouterSuite) SetUpTest(c *check.C) {
s.stub = arvadostest.APIStub{}
s.rtr = &router{
- mux: mux.NewRouter(),
- fed: &s.stub,
+ mux: mux.NewRouter(),
+ backend: &s.stub,
}
s.rtr.addRoutes()
}
cluster.TLS.Insecure = true
arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
url, _ := url.Parse("https://" + os.Getenv("ARVADOS_TEST_API_HOST"))
- s.rtr = New(rpc.NewConn("zzzzz", url, true, rpc.PassthroughTokenProvider))
+ s.rtr = New(rpc.NewConn("zzzzz", url, true, rpc.PassthroughTokenProvider), nil)
}
func (s *RouterIntegrationSuite) TearDownSuite(c *check.C) {
--- /dev/null
+pam_arvados.h
+pam_arvados.so
--- /dev/null
+For configuration advice, please refer to https://doc.arvados.org/install/install-webshell.html
+
+Usage (in pam config):
+
+ pam_arvados.so arvados_api_host my_vm_hostname ["insecure"] ["debug"]
+
+pam_arvados.so passes authentication if (according to
+arvados_api_host) the supplied PAM token belongs to an Arvados user
+who is allowed to log in to my_vm_host_name with the supplied PAM
+username.
+
+If my_vm_hostname is omitted or "-", the current hostname is used.
+
+"insecure" -- continue even if the TLS certificate presented by
+arvados_api_host fails verification.
+
+"debug" -- enable debug-level log messages in syslog and (when not in
+"silent" mode) on the calling application's stderr.
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "bytes"
+ "crypto/tls"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "git.arvados.org/arvados.git/sdk/go/arvadostest"
+ "gopkg.in/check.v1"
+)
+
+type DockerSuite struct {
+ tmpdir string
+ hostip string
+ proxyln net.Listener
+ proxysrv *http.Server
+}
+
+var _ = check.Suite(&DockerSuite{})
+
+func Test(t *testing.T) { check.TestingT(t) }
+
+func (s *DockerSuite) SetUpSuite(c *check.C) {
+ if testing.Short() {
+ c.Skip("skipping docker tests in short mode")
+ } else if _, err := exec.Command("docker", "info").CombinedOutput(); err != nil {
+ c.Skip("skipping docker tests because docker is not available")
+ }
+
+ s.tmpdir = c.MkDir()
+
+ // The integration-testing controller listens on the loopback
+ // interface, so it won't be reachable directly from the
+ // docker container -- so here we run a proxy on 0.0.0.0 for
+ // the duration of the test.
+ hostips, err := exec.Command("hostname", "-I").Output()
+ c.Assert(err, check.IsNil)
+ s.hostip = strings.Split(strings.Trim(string(hostips), "\n"), " ")[0]
+ ln, err := net.Listen("tcp", s.hostip+":0")
+ c.Assert(err, check.IsNil)
+ s.proxyln = ln
+ proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")})
+ proxy.Transport = &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ }
+ s.proxysrv = &http.Server{Handler: proxy}
+ go s.proxysrv.ServeTLS(ln, "../../services/api/tmp/self-signed.pem", "../../services/api/tmp/self-signed.key")
+
+ // Build a pam module to install & configure in the docker
+ // container.
+ cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", s.tmpdir+"/pam_arvados.so")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ c.Assert(err, check.IsNil)
+
+ // Build the testclient program that will (from inside the
+ // docker container) configure the system to use the above PAM
+ // config, and then try authentication.
+ cmd = exec.Command("go", "build", "-o", s.tmpdir+"/testclient", "./testclient.go")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ c.Assert(err, check.IsNil)
+}
+
+func (s *DockerSuite) TearDownSuite(c *check.C) {
+ if s.proxysrv != nil {
+ s.proxysrv.Close()
+ }
+ if s.proxyln != nil {
+ s.proxyln.Close()
+ }
+}
+
+func (s *DockerSuite) SetUpTest(c *check.C) {
+ // Write a PAM config file that uses our proxy as
+ // ARVADOS_API_HOST.
+ proxyhost := s.proxyln.Addr().String()
+ confdata := fmt.Sprintf(`Name: Arvados authentication
+Default: yes
+Priority: 256
+Auth-Type: Primary
+Auth:
+ [success=end default=ignore] /usr/lib/pam_arvados.so %s testvm2.shell insecure
+Auth-Initial:
+ [success=end default=ignore] /usr/lib/pam_arvados.so %s testvm2.shell insecure
+`, proxyhost, proxyhost)
+ err := ioutil.WriteFile(s.tmpdir+"/conffile", []byte(confdata), 0755)
+ c.Assert(err, check.IsNil)
+}
+
+func (s *DockerSuite) runTestClient(c *check.C, args ...string) (stdout, stderr *bytes.Buffer, err error) {
+
+ cmd := exec.Command("docker", append([]string{
+ "run", "--rm",
+ "--hostname", "testvm2.shell",
+ "--add-host", "zzzzz.arvadosapi.com:" + s.hostip,
+ "-v", s.tmpdir + "/pam_arvados.so:/usr/lib/pam_arvados.so:ro",
+ "-v", s.tmpdir + "/conffile:/usr/share/pam-configs/arvados:ro",
+ "-v", s.tmpdir + "/testclient:/testclient:ro",
+ "debian:buster",
+ "/testclient"}, args...)...)
+ stdout = &bytes.Buffer{}
+ stderr = &bytes.Buffer{}
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ err = cmd.Run()
+ return
+}
+
+func (s *DockerSuite) TestSuccess(c *check.C) {
+ stdout, stderr, err := s.runTestClient(c, "try", "active", arvadostest.ActiveTokenV2)
+ c.Check(err, check.IsNil)
+ c.Logf("%s", stderr.String())
+ c.Check(stdout.String(), check.Equals, "")
+ c.Check(stderr.String(), check.Matches, `(?ms).*authentication succeeded.*`)
+}
+
+func (s *DockerSuite) TestFailure(c *check.C) {
+ for _, trial := range []struct {
+ label string
+ username string
+ token string
+ }{
+ {"bad token", "active", arvadostest.ActiveTokenV2 + "badtoken"},
+ {"empty token", "active", ""},
+ {"empty username", "", arvadostest.ActiveTokenV2},
+ {"wrong username", "wrongusername", arvadostest.ActiveTokenV2},
+ } {
+ c.Logf("trial: %s", trial.label)
+ stdout, stderr, err := s.runTestClient(c, "try", trial.username, trial.token)
+ c.Logf("%s", stderr.String())
+ c.Check(err, check.NotNil)
+ c.Check(stdout.String(), check.Equals, "")
+ c.Check(stderr.String(), check.Matches, `(?ms).*authentication failed.*`)
+ }
+}
+
+func (s *DockerSuite) TestDefaultHostname(c *check.C) {
+ confdata := fmt.Sprintf(`Name: Arvados authentication
+Default: yes
+Priority: 256
+Auth-Type: Primary
+Auth:
+ [success=end default=ignore] /usr/lib/pam_arvados.so %s - insecure debug
+Auth-Initial:
+ [success=end default=ignore] /usr/lib/pam_arvados.so %s - insecure debug
+`, s.proxyln.Addr().String(), s.proxyln.Addr().String())
+ err := ioutil.WriteFile(s.tmpdir+"/conffile", []byte(confdata), 0755)
+ c.Assert(err, check.IsNil)
+
+ stdout, stderr, err := s.runTestClient(c, "try", "active", arvadostest.ActiveTokenV2)
+ c.Check(err, check.IsNil)
+ c.Logf("%s", stderr.String())
+ c.Check(stdout.String(), check.Equals, "")
+ c.Check(stderr.String(), check.Matches, `(?ms).*authentication succeeded.*`)
+}
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+fpm_depends+=(ca-certificates)
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# This file is packaged as /usr/share/pam-configs/arvados-go; see build/run-library.sh
+
+# 1. Run `pam-auth-update` and choose Arvados authentication
+# 2. In /etc/pam.d/common-auth, change "api.example" to your ARVADOS_API_HOST
+# 3. In /etc/pam.d/common-auth, change "shell.example" to this host's hostname
+# (as it appears in the Arvados virtual_machines list)
+
+Name: Arvados authentication
+Default: yes
+Priority: 256
+Auth-Type: Primary
+Auth:
+ [success=end default=ignore] /usr/lib/pam_arvados.so api.example shell.example
+Auth-Initial:
+ [success=end default=ignore] /usr/lib/pam_arvados.so api.example shell.example
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// To enable, add an entry in /etc/pam.d/common-auth where pam_unix.so
+// would normally be. Examples:
+//
+// auth [success=1 default=ignore] /usr/lib/pam_arvados.so zzzzz.arvadosapi.com vmhostname.example
+// auth [success=1 default=ignore] /usr/lib/pam_arvados.so zzzzz.arvadosapi.com vmhostname.example insecure debug
+//
+// Replace zzzzz.arvadosapi.com with your controller host or
+// host:port.
+//
+// Replace vmhostname.example with the VM's name as it appears in the
+// Arvados virtual_machine object.
+//
+// Use "insecure" if your API server certificate does not pass name
+// verification.
+//
+// Use "debug" to enable debug log messages.
+
+package main
+
+import (
+ "io/ioutil"
+ "log/syslog"
+ "os"
+
+ "context"
+ "errors"
+ "fmt"
+ "runtime"
+ "syscall"
+ "time"
+
+ "git.arvados.org/arvados.git/sdk/go/arvados"
+ "github.com/sirupsen/logrus"
+ lSyslog "github.com/sirupsen/logrus/hooks/syslog"
+ "golang.org/x/sys/unix"
+)
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+#include <security/pam_ext.h>
+char *stringindex(char** a, int i);
+const char *get_user(pam_handle_t *pamh);
+const char *get_authtoken(pam_handle_t *pamh);
+*/
+import "C"
+
+func main() {}
+
+func init() {
+ if err := unix.Prctl(syscall.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil {
+ newLogger(false).WithError(err).Warn("unable to disable ptrace")
+ }
+}
+
+//export pam_sm_setcred
+func pam_sm_setcred(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
+ return C.PAM_IGNORE
+}
+
+//export pam_sm_authenticate
+func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
+ runtime.GOMAXPROCS(1)
+ logger := newLogger(flags&C.PAM_SILENT == 0)
+ cUsername := C.get_user(pamh)
+ if cUsername == nil {
+ return C.PAM_USER_UNKNOWN
+ }
+
+ cToken := C.get_authtoken(pamh)
+ if cToken == nil {
+ return C.PAM_AUTH_ERR
+ }
+
+ argv := make([]string, cArgc)
+ for i := 0; i < int(cArgc); i++ {
+ argv[i] = C.GoString(C.stringindex(cArgv, C.int(i)))
+ }
+
+ err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv)
+ if err != nil {
+ logger.WithError(err).Error("authentication failed")
+ return C.PAM_AUTH_ERR
+ }
+ return C.PAM_SUCCESS
+}
+
+func authenticate(logger *logrus.Logger, username, token string, argv []string) error {
+ hostname := ""
+ apiHost := ""
+ insecure := false
+ for idx, arg := range argv {
+ if idx == 0 {
+ apiHost = arg
+ } else if idx == 1 {
+ hostname = arg
+ } else if arg == "insecure" {
+ insecure = true
+ } else if arg == "debug" {
+ logger.SetLevel(logrus.DebugLevel)
+ } else {
+ logger.Warnf("unkown option: %s\n", arg)
+ }
+ }
+ if hostname == "" || hostname == "-" {
+ h, err := os.Hostname()
+ if err != nil {
+ logger.WithError(err).Warnf("cannot get hostname -- try using an explicit hostname in pam config")
+ return fmt.Errorf("cannot get hostname: %w", err)
+ }
+ hostname = h
+ }
+ logger.Debugf("username=%q arvados_api_host=%q hostname=%q insecure=%t", username, apiHost, hostname, insecure)
+ if apiHost == "" {
+ logger.Warnf("cannot authenticate: config error: arvados_api_host and hostname must be non-empty")
+ return errors.New("config error")
+ }
+ arv := &arvados.Client{
+ Scheme: "https",
+ APIHost: apiHost,
+ AuthToken: token,
+ Insecure: insecure,
+ }
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
+ defer cancel()
+ var vms arvados.VirtualMachineList
+ err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{
+ Limit: 2,
+ Filters: []arvados.Filter{
+ {"hostname", "=", hostname},
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if len(vms.Items) == 0 {
+ // It's possible there is no VM entry for the
+ // configured hostname, but typically this just means
+ // the user does not have permission to see (let alone
+ // log in to) this VM.
+ return errors.New("permission denied")
+ } else if len(vms.Items) > 1 {
+ return fmt.Errorf("multiple results for hostname %q", hostname)
+ } else if vms.Items[0].Hostname != hostname {
+ return fmt.Errorf("looked up hostname %q but controller returned record with hostname %q", hostname, vms.Items[0].Hostname)
+ }
+ var user arvados.User
+ err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil)
+ if err != nil {
+ return err
+ }
+ var links arvados.LinkList
+ err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{
+ Limit: 1,
+ Filters: []arvados.Filter{
+ {"link_class", "=", "permission"},
+ {"name", "=", "can_login"},
+ {"tail_uuid", "=", user.UUID},
+ {"head_uuid", "=", vms.Items[0].UUID},
+ {"properties.username", "=", username},
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if len(links.Items) < 1 || links.Items[0].Properties["username"] != username {
+ return errors.New("permission denied")
+ }
+ logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID)
+ return nil
+}
+
+func newLogger(stderr bool) *logrus.Logger {
+ logger := logrus.New()
+ if !stderr {
+ logger.Out = ioutil.Discard
+ }
+ if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil {
+ logger.Hooks.Add(hook)
+ }
+ return logger
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+#include <security/pam_ext.h>
+char *stringindex(char** a, int i) { return a[i]; }
+const char *get_user(pam_handle_t *pamh) {
+ const char *user;
+ if (pam_get_item(pamh, PAM_USER, (const void**)&user) != PAM_SUCCESS)
+ return NULL;
+ return user;
+}
+const char *get_authtoken(pam_handle_t *pamh) {
+ const char *token;
+ if (pam_get_authtok(pamh, PAM_AUTHTOK, &token, NULL) != PAM_SUCCESS)
+ return NULL;
+ return token;
+}
+*/
+import "C"
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// +build never
+
+// This file is compiled by docker_test.go to build a test client.
+// It's not part of the pam module itself.
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+
+ "github.com/msteinert/pam"
+ "github.com/sirupsen/logrus"
+)
+
+func main() {
+ if len(os.Args) != 4 || os.Args[1] != "try" {
+ logrus.Print("usage: testclient try 'username' 'password'")
+ os.Exit(1)
+ }
+ username := os.Args[2]
+ password := os.Args[3]
+
+ // Configure PAM to use arvados token auth by default.
+ cmd := exec.Command("pam-auth-update", "--force", "arvados", "--remove", "unix")
+ cmd.Env = append([]string{"DEBIAN_FRONTEND=noninteractive"}, os.Environ()...)
+ cmd.Stdin = nil
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err := cmd.Run()
+ if err != nil {
+ logrus.WithError(err).Error("pam-auth-update failed")
+ os.Exit(1)
+ }
+
+ // Check that pam-auth-update actually added arvados config.
+ cmd = exec.Command("grep", "-Hn", "arvados", "/etc/pam.d/common-auth")
+ cmd.Stdout = os.Stderr
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ if err != nil {
+ panic(err)
+ }
+
+ logrus.Debugf("starting pam: username=%q password=%q", username, password)
+
+ sentPassword := false
+ errorMessage := ""
+ tx, err := pam.StartFunc("default", username, func(style pam.Style, message string) (string, error) {
+ logrus.Debugf("pam conversation: style=%v message=%q", style, message)
+ switch style {
+ case pam.ErrorMsg:
+ logrus.WithField("Message", message).Info("pam.ErrorMsg")
+ errorMessage = message
+ return "", nil
+ case pam.TextInfo:
+ logrus.WithField("Message", message).Info("pam.TextInfo")
+ errorMessage = message
+ return "", nil
+ case pam.PromptEchoOn, pam.PromptEchoOff:
+ sentPassword = true
+ return password, nil
+ default:
+ return "", fmt.Errorf("unrecognized message style %d", style)
+ }
+ })
+ if err != nil {
+ logrus.WithError(err).Print("StartFunc failed")
+ os.Exit(1)
+ }
+ err = tx.Authenticate(pam.DisallowNullAuthtok)
+ if err != nil {
+ err = fmt.Errorf("PAM: %s (message = %q)", err, errorMessage)
+ logrus.WithError(err).Print("authentication failed")
+ os.Exit(1)
+ }
+ logrus.Print("authentication succeeded")
+}
if (!requireNamespace("markdown")) {
install.packages("markdown")
}
+if (!requireNamespace("XML")) {
+ # XML 3.99-0.4 depends on R >= 4.0.0, but we run tests on debian
+ # stable (10) with R 3.5.2 so we install an older version from
+ # source.
+ install.packages("https://cran.r-project.org/src/contrib/Archive/XML/XML_3.99-0.3.tar.gz", repos=NULL, type="source")
+}
devtools::install_dev_deps()
def visit(v, cur_id):
if isinstance(v, dict):
if v.get("class") in ("CommandLineTool", "Workflow"):
+ if tool.metadata["cwlVersion"] == "v1.0" and "id" not in v:
+ raise SourceLine(v, None, Exception).makeError("Embedded process object is missing required 'id' field, add an 'id' or use to cwlVersion: v1.1")
if "id" in v:
cur_id = rewrite_to_orig.get(v["id"], v["id"])
if "path" in v and "location" not in v:
output:
out: null
tool: wf-defaults/wf4.cwl
- doc: default in embedded subworkflow missing 'id' field
+ doc: default in embedded subworkflow missing 'id' field, v1.0
should_fail: true
+- job: null
+ output:
+ out: null
+ tool: wf-defaults/wf8.cwl
+ doc: default in embedded subworkflow missing 'id' field, v1.1
+ should_fail: false
+
- job: null
output:
out: null
class: Directory
location: inp1
outputs: []
- arguments: [echo, $(inputs.inp2)]
\ No newline at end of file
+ arguments: [echo, $(inputs.inp2)]
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+cwlVersion: v1.1
+class: Workflow
+inputs: []
+outputs: []
+$namespaces:
+ arv: "http://arvados.org/cwl#"
+steps:
+ step1:
+ in: []
+ out: []
+ run:
+ class: CommandLineTool
+ inputs:
+ inp2:
+ type: Directory
+ default:
+ class: Directory
+ location: inp1
+ outputs: []
+ arguments: [echo, $(inputs.inp2)]
step1:
in: []
out: []
- run: default-dir4.cwl
\ No newline at end of file
+ run: default-dir4.cwl
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+cwlVersion: v1.1
+class: Workflow
+inputs: []
+outputs: []
+$namespaces:
+ arv: "http://arvados.org/cwl#"
+requirements:
+ SubworkflowFeatureRequirement: {}
+steps:
+ step1:
+ in: []
+ out: []
+ run: default-dir8.cwl
// Link is an arvados#link record
type Link struct {
- UUID string `json:"uuid,omiempty"`
- OwnerUUID string `json:"owner_uuid"`
- Name string `json:"name"`
- LinkClass string `json:"link_class"`
- HeadUUID string `json:"head_uuid"`
- HeadKind string `json:"head_kind"`
- TailUUID string `json:"tail_uuid"`
- TailKind string `json:"tail_kind"`
+ UUID string `json:"uuid,omiempty"`
+ OwnerUUID string `json:"owner_uuid"`
+ Name string `json:"name"`
+ LinkClass string `json:"link_class"`
+ HeadUUID string `json:"head_uuid"`
+ HeadKind string `json:"head_kind"`
+ TailUUID string `json:"tail_uuid"`
+ TailKind string `json:"tail_kind"`
+ Properties map[string]interface{} `json:"properties"`
}
// UserList is an arvados#userList resource.
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import "time"
+
+// VirtualMachine is an arvados#virtualMachine resource.
+type VirtualMachine struct {
+ UUID string `json:"uuid"`
+ OwnerUUID string `json:"owner_uuid"`
+ Hostname string `json:"hostname"`
+ CreatedAt *time.Time `json:"created_at"`
+ ModifiedAt *time.Time `json:"modified_at"`
+ ModifiedByUserUUID string `json:"modified_by_user_uuid"`
+}
+
+// VirtualMachineList is an arvados#virtualMachineList resource.
+type VirtualMachineList struct {
+ Items []VirtualMachine `json:"items"`
+ ItemsAvailable int `json:"items_available"`
+ Offset int `json:"offset"`
+ Limit int `json:"limit"`
+}