return conn.local.UserBatchUpdate(ctx, options)
}
+func (conn *Conn) UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+ return conn.local.UserAuthenticate(ctx, options)
+}
+
func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
}
s.cluster.Login.GoogleClientID = "zzzzzzzzzzzzzz"
s.addHTTPRemote(c, "zhome", &arvadostest.APIStub{})
s.cluster.Login.LoginCluster = "zhome"
+ // s.fed is already set by SetUpTest, but we need to
+ // reinitialize with the above config changes.
+ s.fed = New(s.cluster)
returnTo := "https://app.example.com/foo?bar"
for _, trial := range []struct {
mux.Handle("/arvados/v1/collections/", rtr)
mux.Handle("/arvados/v1/users", rtr)
mux.Handle("/arvados/v1/users/", rtr)
+ mux.Handle("/"+arvados.EndpointUserAuthenticate.Path, rtr)
mux.Handle("/login", rtr)
mux.Handle("/logout", rtr)
}
func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
return conn.loginController.Login(ctx, opts)
}
+
+func (conn *Conn) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+ return conn.loginController.UserAuthenticate(ctx, opts)
+}
type loginController interface {
Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error)
Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error)
+ UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error)
}
func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) loginController {
func (ctrl errorLoginController) Logout(context.Context, arvados.LogoutOptions) (arvados.LogoutResponse, error) {
return arvados.LogoutResponse{}, ctrl.error
}
+func (ctrl errorLoginController) UserAuthenticate(context.Context, arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+ return arvados.APIClientAuthorization{}, ctrl.error
+}
func noopLogout(cluster *arvados.Cluster, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
target := opts.ReturnTo
"encoding/base64"
"errors"
"fmt"
+ "net/http"
"net/url"
"strings"
"sync"
"git.arvados.org/arvados.git/sdk/go/arvados"
"git.arvados.org/arvados.git/sdk/go/auth"
"git.arvados.org/arvados.git/sdk/go/ctxlog"
+ "git.arvados.org/arvados.git/sdk/go/httpserver"
"github.com/coreos/go-oidc"
"golang.org/x/oauth2"
"google.golang.org/api/option"
}
}
+func (ctrl *googleLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+ return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New("username/password authentication is not available"), http.StatusBadRequest)
+}
+
// Use a person's token to get all of their email addresses, with the
// primary address at index 0. The provided defaultAddr is always
// included in the returned slice, and is used as the primary if the
}
func (ctrl *pamLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
+ return arvados.LoginResponse{}, errors.New("interactive login is not available")
+}
+
+func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
errorMessage := ""
tx, err := pam.StartFunc(ctrl.Cluster.Login.PAMService, opts.Username, func(style pam.Style, message string) (string, error) {
ctxlog.FromContext(ctx).Debugf("pam conversation: style=%v message=%q", style, message)
}
})
if err != nil {
- return arvados.LoginResponse{}, err
+ return arvados.APIClientAuthorization{}, err
}
err = tx.Authenticate(pam.DisallowNullAuthtok)
if err != nil {
- return arvados.LoginResponse{}, httpserver.ErrorWithStatus(err, http.StatusUnauthorized)
+ return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(err, http.StatusUnauthorized)
}
if errorMessage != "" {
- return arvados.LoginResponse{}, httpserver.ErrorWithStatus(errors.New(errorMessage), http.StatusUnauthorized)
+ return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New(errorMessage), http.StatusUnauthorized)
}
user, err := tx.GetItem(pam.User)
if err != nil {
- return arvados.LoginResponse{}, err
+ return arvados.APIClientAuthorization{}, err
}
email := user
if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
// Send a fake ReturnTo value instead of the caller's
// opts.ReturnTo. We won't follow the resulting
// redirect target anyway.
- ReturnTo: opts.Remote + ",https://none.invalid",
+ ReturnTo: ",https://none.invalid",
AuthInfo: rpc.UserSessionAuthInfo{
Username: user,
Email: email,
},
})
if err != nil {
- return arvados.LoginResponse{}, err
+ return arvados.APIClientAuthorization{}, err
}
target, err := url.Parse(resp.RedirectLocation)
if err != nil {
- return arvados.LoginResponse{}, err
+ return arvados.APIClientAuthorization{}, err
}
- resp.Token = target.Query().Get("api_token")
- resp.RedirectLocation = ""
- return resp, err
+ return arvados.APIClientAuthorization{APIToken: target.Query().Get("api_token")}, err
}
}
echo >&2 "Testing authentication failure"
-resp="$(curl -s --include -H "X-Http-Method-Override: GET" -d username=foo -d password=nosecret "http://${ctrlhostport}/login" | tee $debug)"
+resp="$(curl -s --include -d username=foo -d password=nosecret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)"
check_contains "${resp}" "HTTP/1.1 401"
check_contains "${resp}" '{"errors":["Authentication failure"]}'
echo >&2 "Testing authentication success"
-resp="$(curl -s --include -H "X-Http-Method-Override: GET" -d username=foo -d password=secret "http://${ctrlhostport}/login" | tee $debug)"
+resp="$(curl -s --include -d username=foo -d password=secret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)"
check_contains "${resp}" "HTTP/1.1 200"
-check_contains "${resp}" '{"token":"v2/zzzzz-gj3su-'
+check_contains "${resp}" '{"api_token":"v2/zzzzz-gj3su-'
cleanup
}
func (s *PamSuite) TestLoginFailure(c *check.C) {
- resp, err := s.ctrl.Login(context.Background(), arvados.LoginOptions{
+ resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
Username: "bogususername",
Password: "boguspassword",
- ReturnTo: "https://example.com/foo",
})
c.Check(err, check.ErrorMatches, "Authentication failure")
hs, ok := err.(interface{ HTTPStatus() int })
if c.Check(ok, check.Equals, true) {
c.Check(hs.HTTPStatus(), check.Equals, http.StatusUnauthorized)
}
- c.Check(resp.RedirectLocation, check.Equals, "")
- c.Check(resp.Token, check.Equals, "")
- c.Check(resp.Message, check.Equals, "")
- c.Check(resp.HTML.String(), check.Equals, "")
+ c.Check(resp.APIToken, check.Equals, "")
}
// This test only runs if the ARVADOS_TEST_PAM_CREDENTIALS_FILE env
c.Assert(len(lines), check.Equals, 2, check.Commentf("credentials file %s should contain \"username\\npassword\"", testCredsFile))
u, p := lines[0], lines[1]
- resp, err := s.ctrl.Login(context.Background(), arvados.LoginOptions{
+ resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
Username: u,
Password: p,
- ReturnTo: "https://example.com/foo",
})
c.Check(err, check.IsNil)
- c.Check(resp.RedirectLocation, check.Equals, "")
- c.Check(resp.Token, check.Matches, `v2/zzzzz-gj3su-.*/.*`)
- c.Check(resp.HTML.String(), check.Equals, "")
+ c.Check(resp.APIToken, check.Matches, `v2/zzzzz-gj3su-.*/.*`)
authinfo := getCallbackAuthInfo(c, s.railsSpy)
c.Check(authinfo.Email, check.Equals, u+"@"+s.cluster.Login.PAMDefaultEmailDomain)
return rtr.fed.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))
+ },
+ },
} {
rtr.addRoute(route.endpoint, route.defaultOpts, route.exec)
}
}
func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
- ep := arvados.APIEndpoint{Method: "PATCH", Path: "arvados/v1/users/batch_update"}
+ ep := arvados.EndpointUserBatchUpdate
var resp arvados.UserList
err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
return resp, err
}
+
+func (conn *Conn) UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+ ep := arvados.EndpointUserAuthenticate
+ var resp arvados.APIClientAuthorization
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
EndpointUserUpdate = APIEndpoint{"PATCH", "arvados/v1/users/{uuid}", "user"}
EndpointUserUpdateUUID = APIEndpoint{"POST", "arvados/v1/users/{uuid}/update_uuid", ""}
EndpointUserBatchUpdate = APIEndpoint{"PATCH", "arvados/v1/users/batch", ""}
+ EndpointUserAuthenticate = APIEndpoint{"POST", "arvados/v1/users/authenticate", ""}
EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
)
}
type LoginOptions struct {
- ReturnTo string `json:"return_to"` // On success, redirect to this target with api_token=xxx query param
- Remote string `json:"remote,omitempty"` // Salt token for remote Cluster ID
- Code string `json:"code,omitempty"` // OAuth2 callback code
- State string `json:"state,omitempty"` // OAuth2 callback state
+ ReturnTo string `json:"return_to"` // On success, redirect to this target with api_token=xxx query param
+ Remote string `json:"remote,omitempty"` // Salt token for remote Cluster ID
+ Code string `json:"code,omitempty"` // OAuth2 callback code
+ State string `json:"state,omitempty"` // OAuth2 callback state
+}
+
+type UserAuthenticateOptions struct {
Username string `json:"username,omitempty"` // PAM username
Password string `json:"password,omitempty"` // PAM password
}
UserList(ctx context.Context, options ListOptions) (UserList, error)
UserDelete(ctx context.Context, options DeleteOptions) (User, error)
UserBatchUpdate(context.Context, UserBatchUpdateOptions) (UserList, error)
+ UserAuthenticate(ctx context.Context, options UserAuthenticateOptions) (APIClientAuthorization, error)
APIClientAuthorizationCurrent(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
}
import (
"bytes"
- "encoding/json"
"net/http"
)
type LoginResponse struct {
RedirectLocation string `json:"redirect_location,omitempty"`
- Token string `json:"token,omitempty"`
- Message string `json:"message,omitempty"`
HTML bytes.Buffer `json:"-"`
}
if resp.RedirectLocation != "" {
w.Header().Set("Location", resp.RedirectLocation)
w.WriteHeader(http.StatusFound)
- } else if resp.Token != "" || resp.Message != "" {
- w.Header().Set("Content-Type", "application/json")
- if resp.Token == "" {
- w.WriteHeader(http.StatusUnauthorized)
- }
- json.NewEncoder(w).Encode(resp)
} else {
w.Header().Set("Content-Type", "text/html")
w.Write(resp.HTML.Bytes())
as.appendCall(as.UserBatchUpdate, ctx, options)
return arvados.UserList{}, as.Error
}
+func (as *APIStub) UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+ as.appendCall(as.UserAuthenticate, ctx, options)
+ return arvados.APIClientAuthorization{}, as.Error
+}
func (as *APIStub) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
as.appendCall(as.APIClientAuthorizationCurrent, ctx, options)
return arvados.APIClientAuthorization{}, as.Error