17944: Fixes test. Avoids unnecessary Validate() call.
[arvados.git] / lib / controller / localdb / conn.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         "encoding/json"
10         "fmt"
11         "net/http"
12         "os"
13         "strings"
14         "time"
15
16         "git.arvados.org/arvados.git/lib/controller/railsproxy"
17         "git.arvados.org/arvados.git/lib/controller/rpc"
18         "git.arvados.org/arvados.git/sdk/go/arvados"
19         "git.arvados.org/arvados.git/sdk/go/ctxlog"
20         "git.arvados.org/arvados.git/sdk/go/httpserver"
21 )
22
23 type railsProxy = rpc.Conn
24
25 type Conn struct {
26         cluster                    *arvados.Cluster
27         *railsProxy                // handles API methods that aren't defined on Conn itself
28         vocabularyCache            *arvados.Vocabulary
29         vocabularyFileModTime      time.Time
30         lastVocabularyRefreshCheck time.Time
31         lastVocabularyError        error
32         loginController
33 }
34
35 func NewConn(cluster *arvados.Cluster) *Conn {
36         railsProxy := railsproxy.NewConn(cluster)
37         railsProxy.RedactHostInErrors = true
38         conn := Conn{
39                 cluster:    cluster,
40                 railsProxy: railsProxy,
41         }
42         conn.loginController = chooseLoginController(cluster, &conn)
43         return &conn
44 }
45
46 func (conn *Conn) checkProperties(ctx context.Context, properties interface{}) error {
47         if properties == nil {
48                 return nil
49         }
50         var props map[string]interface{}
51         switch properties := properties.(type) {
52         case string:
53                 err := json.Unmarshal([]byte(properties), &props)
54                 if err != nil {
55                         return err
56                 }
57         case map[string]interface{}:
58                 props = properties
59         default:
60                 return fmt.Errorf("unexpected properties type %T", properties)
61         }
62         voc, err := conn.VocabularyGet(ctx)
63         if err != nil {
64                 return err
65         }
66         err = voc.Check(props)
67         if err != nil {
68                 return httpErrorf(http.StatusBadRequest, voc.Check(props).Error())
69         }
70         return nil
71 }
72
73 func (conn *Conn) maybeRefreshVocabularyCache() error {
74         if conn.lastVocabularyRefreshCheck.Add(time.Second).After(time.Now()) {
75                 // Throttle the access to disk to at most once per second.
76                 return nil
77         }
78         conn.lastVocabularyRefreshCheck = time.Now()
79         fi, err := os.Stat(conn.cluster.API.VocabularyPath)
80         if err != nil {
81                 err = fmt.Errorf("couldn't stat vocabulary file %q: %v", conn.cluster.API.VocabularyPath, err)
82                 conn.lastVocabularyError = err
83                 return err
84         }
85         if fi.ModTime().After(conn.vocabularyFileModTime) {
86                 err = conn.loadVocabularyFile()
87                 if err != nil {
88                         conn.lastVocabularyError = err
89                         return err
90                 }
91                 conn.vocabularyFileModTime = fi.ModTime()
92                 conn.lastVocabularyError = nil
93         }
94         return nil
95 }
96
97 func (conn *Conn) loadVocabularyFile() error {
98         vf, err := os.ReadFile(conn.cluster.API.VocabularyPath)
99         if err != nil {
100                 return fmt.Errorf("couldn't reading the vocabulary file: %v", err)
101         }
102         mk := make([]string, 0, len(conn.cluster.Collections.ManagedProperties))
103         for k := range conn.cluster.Collections.ManagedProperties {
104                 mk = append(mk, k)
105         }
106         voc, err := arvados.NewVocabulary(vf, mk)
107         if err != nil {
108                 return fmt.Errorf("while loading vocabulary file %q: %s", conn.cluster.API.VocabularyPath, err)
109         }
110         conn.vocabularyCache = voc
111         return nil
112 }
113
114 // LastVocabularyError returns the last error encountered while loading the
115 // vocabulary file.
116 func (conn *Conn) LastVocabularyError() error {
117         conn.maybeRefreshVocabularyCache()
118         return conn.lastVocabularyError
119 }
120
121 // VocabularyGet refreshes the vocabulary cache if necessary and returns it.
122 func (conn *Conn) VocabularyGet(ctx context.Context) (arvados.Vocabulary, error) {
123         if conn.cluster.API.VocabularyPath == "" {
124                 return arvados.Vocabulary{
125                         Tags: map[string]arvados.VocabularyTag{},
126                 }, nil
127         }
128         logger := ctxlog.FromContext(ctx)
129         if conn.vocabularyCache == nil {
130                 // Initial load of vocabulary file.
131                 err := conn.loadVocabularyFile()
132                 if err != nil {
133                         logger.WithError(err).Error("error loading vocabulary file")
134                         return arvados.Vocabulary{}, err
135                 }
136         }
137         err := conn.maybeRefreshVocabularyCache()
138         if err != nil {
139                 logger.WithError(err).Error("error reloading vocabulary file - ignoring")
140         } else {
141                 logger.Info("vocabulary file reloaded successfully")
142         }
143         return *conn.vocabularyCache, nil
144 }
145
146 // Logout handles the logout of conn giving to the appropriate loginController
147 func (conn *Conn) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
148         return conn.loginController.Logout(ctx, opts)
149 }
150
151 // Login handles the login of conn giving to the appropriate loginController
152 func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
153         return conn.loginController.Login(ctx, opts)
154 }
155
156 // UserAuthenticate handles the User Authentication of conn giving to the appropriate loginController
157 func (conn *Conn) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
158         return conn.loginController.UserAuthenticate(ctx, opts)
159 }
160
161 func (conn *Conn) GroupContents(ctx context.Context, options arvados.GroupContentsOptions) (arvados.ObjectList, error) {
162         // The requested UUID can be a user (virtual home project), which we just pass on to
163         // the API server.
164         if strings.Index(options.UUID, "-j7d0g-") != 5 {
165                 return conn.railsProxy.GroupContents(ctx, options)
166         }
167
168         var resp arvados.ObjectList
169
170         // Get the group object
171         respGroup, err := conn.GroupGet(ctx, arvados.GetOptions{UUID: options.UUID})
172         if err != nil {
173                 return resp, err
174         }
175
176         // If the group has groupClass 'filter', apply the filters before getting the contents.
177         if respGroup.GroupClass == "filter" {
178                 if filters, ok := respGroup.Properties["filters"].([]interface{}); ok {
179                         for _, f := range filters {
180                                 // f is supposed to be a []string
181                                 tmp, ok2 := f.([]interface{})
182                                 if !ok2 || len(tmp) < 3 {
183                                         return resp, fmt.Errorf("filter unparsable: %T, %+v, original field: %T, %+v\n", tmp, tmp, f, f)
184                                 }
185                                 var filter arvados.Filter
186                                 if attr, ok2 := tmp[0].(string); ok2 {
187                                         filter.Attr = attr
188                                 } else {
189                                         return resp, fmt.Errorf("filter unparsable: attribute must be string: %T, %+v, filter: %T, %+v\n", tmp[0], tmp[0], f, f)
190                                 }
191                                 if operator, ok2 := tmp[1].(string); ok2 {
192                                         filter.Operator = operator
193                                 } else {
194                                         return resp, fmt.Errorf("filter unparsable: operator must be string: %T, %+v, filter: %T, %+v\n", tmp[1], tmp[1], f, f)
195                                 }
196                                 filter.Operand = tmp[2]
197                                 options.Filters = append(options.Filters, filter)
198                         }
199                 } else {
200                         return resp, fmt.Errorf("filter unparsable: not an array\n")
201                 }
202                 // Use the generic /groups/contents endpoint for filter groups
203                 options.UUID = ""
204         }
205
206         return conn.railsProxy.GroupContents(ctx, options)
207 }
208
209 func httpErrorf(code int, format string, args ...interface{}) error {
210         return httpserver.ErrorWithStatus(fmt.Errorf(format, args...), code)
211 }