1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
19 "git.arvados.org/arvados.git/lib/controller/api"
20 "git.arvados.org/arvados.git/sdk/go/arvados"
21 "git.arvados.org/arvados.git/sdk/go/auth"
22 "github.com/ghodss/yaml"
26 ErrNoAuthContext = errors.New("bug: there is no authorization in this context")
27 ErrUnauthenticated = errors.New("unauthenticated request")
30 // WrapCallsWithAuth returns a call wrapper (suitable for assigning to
31 // router.router.WrapCalls) that makes CurrentUser(ctx) et al. work
32 // from inside the wrapped functions.
34 // The incoming context must come from WrapCallsInTransactions or
35 // NewWithTransaction.
36 func WrapCallsWithAuth(cluster *arvados.Cluster) func(api.RoutableFunc) api.RoutableFunc {
37 var authcache authcache
38 return func(origFunc api.RoutableFunc) api.RoutableFunc {
39 return func(ctx context.Context, opts interface{}) (_ interface{}, err error) {
41 if creds, ok := auth.FromContext(ctx); ok {
44 return origFunc(context.WithValue(ctx, contextKeyAuth, &authcontext{
45 authcache: &authcache,
53 // CurrentAuth returns the arvados.User whose privileges should be
54 // used in the given context, and the arvados.APIClientAuthorization
55 // the caller presented in order to authenticate the current request.
57 // Returns ErrUnauthenticated if the current request was not
58 // authenticated (no token provided, token is expired, etc).
59 func CurrentAuth(ctx context.Context) (*arvados.User, *arvados.APIClientAuthorization, error) {
60 ac, ok := ctx.Value(contextKeyAuth).(*authcontext)
62 return nil, nil, ErrNoAuthContext
64 ac.lookupOnce.Do(func() {
65 // We only validate/lookup the token once per API
66 // call, even though authcache should be efficient
67 // enough to do a lookup each time. This guarantees we
68 // always return the same result when called multiple
69 // times in the course of handling a single API call.
70 for _, token := range ac.tokens {
71 user, aca, err := ac.authcache.lookup(ctx, ac.cluster, token)
77 ac.user, ac.apiClientAuthorization = user, aca
81 ac.err = ErrUnauthenticated
83 return ac.user, ac.apiClientAuthorization, ac.err
86 type contextKeyA string
88 var contextKeyAuth = contextKeyT("auth")
90 type authcontext struct {
92 cluster *arvados.Cluster
95 apiClientAuthorization *arvados.APIClientAuthorization
100 var authcacheTTL = time.Minute
102 type authcacheent struct {
104 apiClientAuthorization arvados.APIClientAuthorization
108 type authcache struct {
110 entries map[string]*authcacheent
111 nextCleanup time.Time
114 // lookup returns the user and aca info for a given token. Returns nil
115 // if the token is not valid. Returns a non-nil error if there was an
116 // unexpected error from the database, etc.
117 func (ac *authcache) lookup(ctx context.Context, cluster *arvados.Cluster, token string) (*arvados.User, *arvados.APIClientAuthorization, error) {
119 ent := ac.entries[token]
121 if ent != nil && ent.expireTime.After(time.Now()) {
122 return &ent.user, &ent.apiClientAuthorization, nil
127 tx, err := CurrentTx(ctx)
131 var aca arvados.APIClientAuthorization
132 var user arvados.User
135 var args []interface{}
136 if len(token) > 30 && strings.HasPrefix(token, "v2/") && token[30] == '/' {
137 fields := strings.Split(token, "/")
138 cond = `aca.uuid = $1 and aca.api_token = $2`
139 args = []interface{}{fields[1], fields[2]}
141 // Bare token or OIDC access token
142 mac := hmac.New(sha256.New, []byte(cluster.SystemRootToken))
143 io.WriteString(mac, token)
144 hmac := fmt.Sprintf("%x", mac.Sum(nil))
145 cond = `aca.api_token in ($1, $2)`
146 args = []interface{}{token, hmac}
148 var expiresAt sql.NullTime
149 var scopesYAML []byte
150 err = tx.QueryRowContext(ctx, `
151 select aca.uuid, aca.expires_at, aca.api_token, aca.scopes, users.uuid, users.is_active, users.is_admin
152 from api_client_authorizations aca
153 left join users on aca.user_id = users.id
155 and (expires_at is null or expires_at > current_timestamp at time zone 'UTC')`, args...).Scan(
156 &aca.UUID, &expiresAt, &aca.APIToken, &scopesYAML,
157 &user.UUID, &user.IsActive, &user.IsAdmin)
158 if err == sql.ErrNoRows {
160 } else if err != nil {
163 aca.ExpiresAt = expiresAt.Time
164 if len(scopesYAML) > 0 {
165 err = yaml.Unmarshal(scopesYAML, &aca.Scopes)
167 return nil, nil, fmt.Errorf("loading scopes for %s: %w", aca.UUID, err)
171 expireTime: time.Now().Add(authcacheTTL),
172 apiClientAuthorization: aca,
176 defer ac.mtx.Unlock()
177 if ac.entries == nil {
178 ac.entries = map[string]*authcacheent{}
180 if ac.nextCleanup.IsZero() || ac.nextCleanup.Before(time.Now()) {
181 for token, ent := range ac.entries {
182 if !ent.expireTime.After(time.Now()) {
183 delete(ac.entries, token)
186 ac.nextCleanup = time.Now().Add(authcacheTTL)
188 ac.entries[token] = ent
189 return &ent.user, &ent.apiClientAuthorization, nil