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"
25 ErrNoAuthContext = errors.New("bug: there is no authorization in this context")
26 ErrUnauthenticated = errors.New("unauthenticated request")
29 // WrapCallsWithAuth returns a call wrapper (suitable for assigning to
30 // router.router.WrapCalls) that makes CurrentUser(ctx) et al. work
31 // from inside the wrapped functions.
33 // The incoming context must come from WrapCallsInTransactions or
34 // NewWithTransaction.
35 func WrapCallsWithAuth(cluster *arvados.Cluster) func(api.RoutableFunc) api.RoutableFunc {
36 return func(origFunc api.RoutableFunc) api.RoutableFunc {
37 return func(ctx context.Context, opts interface{}) (_ interface{}, err error) {
39 if creds, ok := auth.FromContext(ctx); ok {
42 return origFunc(context.WithValue(ctx, contextKeyAuth, &authcontext{cluster: cluster, tokens: tokens}), opts)
47 // CurrentAuth returns the arvados.User whose privileges should be
48 // used in the given context, and the arvados.APIClientAuthorization
49 // the caller presented in order to authenticate the current request.
51 // Returns ErrUnauthenticated if the current request was not
52 // authenticated (no token provided, token is expired, etc).
53 func CurrentAuth(ctx context.Context) (*arvados.User, *arvados.APIClientAuthorization, error) {
54 ac, ok := ctx.Value(contextKeyAuth).(*authcontext)
56 return nil, nil, ErrNoAuthContext
58 ac.lookupOnce.Do(func() { ac.user, ac.apiClientAuthorization, ac.err = aclookup(ctx, ac.cluster, ac.tokens) })
59 return ac.user, ac.apiClientAuthorization, ac.err
62 type contextKeyA string
64 var contextKeyAuth = contextKeyT("auth")
66 type authcontext struct {
67 cluster *arvados.Cluster
70 apiClientAuthorization *arvados.APIClientAuthorization
75 func aclookup(ctx context.Context, cluster *arvados.Cluster, tokens []string) (*arvados.User, *arvados.APIClientAuthorization, error) {
77 return nil, nil, ErrUnauthenticated
79 tx, err := CurrentTx(ctx)
83 var aca arvados.APIClientAuthorization
85 for _, token := range tokens {
87 var args []interface{}
90 } else if len(token) > 30 && strings.HasPrefix(token, "v2/") && token[30] == '/' {
91 fields := strings.Split(token, "/")
92 cond = `aca.uuid=$1 and aca.api_token=$2`
93 args = []interface{}{fields[1], fields[2]}
95 // Bare token or OIDC access token
96 mac := hmac.New(sha256.New, []byte(cluster.SystemRootToken))
97 io.WriteString(mac, token)
98 hmac := fmt.Sprintf("%x", mac.Sum(nil))
99 cond = `aca.api_token in ($1, $2)`
100 args = []interface{}{token, hmac}
102 var scopesJSON []byte
103 err = tx.QueryRowContext(ctx, `
104 select aca.uuid, aca.expires_at, aca.api_token, aca.scopes, users.uuid, users.is_active, users.is_admin
105 from api_client_authorizations aca
106 left join users on aca.user_id = users.id
108 and (expires_at is null or expires_at > current_timestamp at time zone 'UTC')`, args...).Scan(
109 &aca.UUID, &aca.ExpiresAt, &aca.APIToken, &scopesJSON,
110 &user.UUID, &user.IsActive, &user.IsAdmin)
111 if err == sql.ErrNoRows {
113 } else if err != nil {
116 if len(scopesJSON) > 0 {
117 err = json.Unmarshal(scopesJSON, &aca.Scopes)
122 return &user, &aca, nil
124 return nil, nil, ErrUnauthenticated