21703: Merge branch 'main' into 21703-collection-update-lock
[arvados.git] / lib / controller / localdb / logout.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "database/sql"
10         "errors"
11         "fmt"
12         "net/http"
13         "strings"
14
15         "git.arvados.org/arvados.git/lib/ctrlctx"
16         "git.arvados.org/arvados.git/sdk/go/arvados"
17         "git.arvados.org/arvados.git/sdk/go/auth"
18         "git.arvados.org/arvados.git/sdk/go/ctxlog"
19         "git.arvados.org/arvados.git/sdk/go/httpserver"
20 )
21
22 func logout(ctx context.Context, cluster *arvados.Cluster, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
23         err := expireAPIClientAuthorization(ctx)
24         if err != nil {
25                 ctxlog.FromContext(ctx).Errorf("attempting to expire token on logout: %q", err)
26                 return arvados.LogoutResponse{}, httpserver.ErrorWithStatus(errors.New("could not expire token on logout"), http.StatusInternalServerError)
27         }
28
29         target := opts.ReturnTo
30         if target == "" {
31                 if cluster.Services.Workbench2.ExternalURL.Host != "" {
32                         target = cluster.Services.Workbench2.ExternalURL.String()
33                 } else {
34                         target = cluster.Services.Workbench1.ExternalURL.String()
35                 }
36         } else if err := validateLoginRedirectTarget(cluster, target); err != nil {
37                 return arvados.LogoutResponse{}, httpserver.ErrorWithStatus(fmt.Errorf("invalid return_to parameter: %s", err), http.StatusBadRequest)
38         }
39         return arvados.LogoutResponse{RedirectLocation: target}, nil
40 }
41
42 func expireAPIClientAuthorization(ctx context.Context) error {
43         creds, ok := auth.FromContext(ctx)
44         if !ok {
45                 // Tests could be passing empty contexts
46                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization: credentials not found from context")
47                 return nil
48         }
49
50         if len(creds.Tokens) == 0 {
51                 // Old client may not have provided the token to expire
52                 return nil
53         }
54
55         tx, err := ctrlctx.CurrentTx(ctx)
56         if err != nil {
57                 return err
58         }
59
60         token := creds.Tokens[0]
61         tokenSecret := token
62         var tokenUuid string
63         if strings.HasPrefix(token, "v2/") {
64                 tokenParts := strings.Split(token, "/")
65                 if len(tokenParts) >= 3 {
66                         tokenUuid = tokenParts[1]
67                         tokenSecret = tokenParts[2]
68                 }
69         }
70
71         var retrievedUuid string
72         err = tx.QueryRowContext(ctx, `SELECT uuid FROM api_client_authorizations WHERE api_token=$1 AND (expires_at IS NULL OR expires_at > current_timestamp AT TIME ZONE 'UTC') LIMIT 1`, tokenSecret).Scan(&retrievedUuid)
73         if err == sql.ErrNoRows {
74                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): not found in database", token)
75                 return nil
76         } else if err != nil {
77                 ctxlog.FromContext(ctx).WithError(err).Debugf("expireAPIClientAuthorization(%s): database error", token)
78                 return err
79         }
80
81         if tokenUuid != "" && retrievedUuid != tokenUuid {
82                 // secret part matches, but UUID doesn't -- somewhat surprising
83                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): secret part found, but with different UUID: %s", tokenSecret, retrievedUuid)
84                 return nil
85         }
86
87         res, err := tx.ExecContext(ctx, "UPDATE api_client_authorizations SET expires_at=current_timestamp AT TIME ZONE 'UTC' WHERE uuid=$1", retrievedUuid)
88         if err != nil {
89                 return err
90         }
91
92         rows, err := res.RowsAffected()
93         if err != nil {
94                 return err
95         }
96         if rows == 0 {
97                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): no rows were updated", tokenSecret)
98                 return fmt.Errorf("couldn't expire provided token")
99         } else if rows > 1 {
100                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): multiple (%d) rows updated", tokenSecret, rows)
101         } else {
102                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): ok", tokenSecret)
103         }
104
105         return nil
106 }