| OLD | NEW |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. | 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package delegation | 5 package delegation |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "fmt" |
| 9 "strings" |
| 10 "time" |
| 11 |
| 12 "github.com/golang/protobuf/jsonpb" |
| 8 "golang.org/x/net/context" | 13 "golang.org/x/net/context" |
| 9 "google.golang.org/grpc" | 14 "google.golang.org/grpc" |
| 10 "google.golang.org/grpc/codes" | 15 "google.golang.org/grpc/codes" |
| 11 | 16 |
| 17 "github.com/luci/luci-go/common/errors" |
| 18 "github.com/luci/luci-go/common/logging" |
| 19 "github.com/luci/luci-go/server/auth" |
| 20 "github.com/luci/luci-go/server/auth/identity" |
| 12 "github.com/luci/luci-go/server/auth/signing" | 21 "github.com/luci/luci-go/server/auth/signing" |
| 13 » minter "github.com/luci/luci-go/tokenserver/api/minter/v1" | 22 |
| 23 » "github.com/luci/luci-go/tokenserver/api/minter/v1" |
| 24 » "github.com/luci/luci-go/tokenserver/appengine/utils/identityset" |
| 14 ) | 25 ) |
| 15 | 26 |
| 16 // MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method. | 27 // MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method. |
| 17 type MintDelegationTokenRPC struct { | 28 type MintDelegationTokenRPC struct { |
| 18 // Signer is mocked in tests. | 29 // Signer is mocked in tests. |
| 19 // | 30 // |
| 20 // In prod it is gaesigner.Signer. | 31 // In prod it is gaesigner.Signer. |
| 21 Signer signing.Signer | 32 Signer signing.Signer |
| 33 |
| 34 // ConfigLoader loads delegation config on demand. |
| 35 // |
| 36 // In prod it is DelegationConfigLoader. |
| 37 ConfigLoader func(context.Context) (*DelegationConfig, error) |
| 22 } | 38 } |
| 23 | 39 |
| 24 // MintDelegationToken generates a new bearer delegation token. | 40 // MintDelegationToken generates a new bearer delegation token. |
| 25 func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *min
ter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) { | 41 func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *min
ter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) { |
| 42 state := auth.GetState(c) |
| 43 |
| 44 // Dump the whole request and relevant auth state to the debug log. |
| 45 if logging.IsLogging(c, logging.Debug) { |
| 46 m := jsonpb.Marshaler{Indent: " "} |
| 47 dump, _ := m.MarshalToString(req) |
| 48 logging.Debugf(c, "PeerIdentity: %s", state.PeerIdentity()) |
| 49 logging.Debugf(c, "MintDelegationTokenRequest:\n%s", dump) |
| 50 } |
| 51 |
| 52 // Validate the request authentication context: not an anonymous call, n
o |
| 53 // delegation is used. |
| 54 callerID := state.User().Identity |
| 55 if callerID != state.PeerIdentity() { |
| 56 logging.Errorf(c, "Trying to use delegation, it's forbidden") |
| 57 return nil, grpc.Errorf(codes.PermissionDenied, "delegation is f
orbidden for this API call") |
| 58 } |
| 59 if callerID == identity.AnonymousIdentity { |
| 60 logging.Errorf(c, "Unauthenticated request") |
| 61 return nil, grpc.Errorf(codes.Unauthenticated, "authentication r
equired") |
| 62 } |
| 63 |
| 64 cfg, err := r.ConfigLoader(c) |
| 65 if err != nil { |
| 66 // Don't put error details in the message, it may be returned to |
| 67 // unauthorized callers. ConfigLoader logs the error already. |
| 68 return nil, grpc.Errorf(codes.Internal, "failed to load delegati
on config") |
| 69 } |
| 70 |
| 71 // Make sure the caller is mentioned in the config before doing anything
else. |
| 72 // This rejects unauthorized callers early. Passing this check doesn't m
ean |
| 73 // that there's a matching rule though, so the request still can be reje
cted |
| 74 // later. |
| 75 switch ok, err := cfg.IsAuthorizedRequestor(c, callerID); { |
| 76 case err != nil: |
| 77 logging.WithError(err).Errorf(c, "IsAuthorizedRequestor failed") |
| 78 return nil, grpc.Errorf(codes.Internal, "failed to check authori
zation") |
| 79 case !ok: |
| 80 logging.Errorf(c, "Didn't pass initial authorization") |
| 81 return nil, grpc.Errorf(codes.PermissionDenied, "not authorized"
) |
| 82 } |
| 83 |
| 84 // Validate and normalize the request. This may do relatively expensive
calls |
| 85 // to resolve "https://<service-url>" entries to "service:<id>" entries. |
| 86 query, err := buildRulesQuery(c, req, callerID) |
| 87 if err != nil { |
| 88 if errors.IsTransient(err) { |
| 89 logging.WithError(err).Errorf(c, "buildRulesQuery failed
") |
| 90 return nil, grpc.Errorf(codes.Internal, "failure when re
solving target service ID - %s", err) |
| 91 } |
| 92 logging.WithError(err).Errorf(c, "Bad request") |
| 93 return nil, grpc.Errorf(codes.InvalidArgument, "bad request - %s
", err) |
| 94 } |
| 95 |
| 96 // Consult the config to find the rule that allows this operation (if an
y). |
| 97 rule, err := cfg.FindMatchingRule(c, query) |
| 98 if err != nil { |
| 99 if errors.IsTransient(err) { |
| 100 logging.WithError(err).Errorf(c, "FindMatchingRule faile
d") |
| 101 return nil, grpc.Errorf(codes.Internal, "failure when ch
ecking rules - %s", err) |
| 102 } |
| 103 logging.WithError(err).Errorf(c, "Didn't pass rules check") |
| 104 return nil, grpc.Errorf(codes.PermissionDenied, "forbidden - %s"
, err) |
| 105 } |
| 106 logging.Infof(c, "Found the matching rule %q in the config rev %s", rule
.Name, cfg.Revision) |
| 107 |
| 108 // TODO(vadimsh): Make the token, record it in the audit log. |
| 109 |
| 26 return nil, grpc.Errorf(codes.Unimplemented, "Not implemented yet") | 110 return nil, grpc.Errorf(codes.Unimplemented, "Not implemented yet") |
| 27 } | 111 } |
| 112 |
| 113 // buildRulesQuery validates the request, extracts and normalizes relevant |
| 114 // fields into RulesQuery object. |
| 115 // |
| 116 // May return transient errors. |
| 117 func buildRulesQuery(c context.Context, req *minter.MintDelegationTokenRequest,
requestor identity.Identity) (*RulesQuery, error) { |
| 118 // Validate 'delegated_identity'. |
| 119 var err error |
| 120 var delegatee identity.Identity |
| 121 if req.DelegatedIdentity == "" { |
| 122 return nil, fmt.Errorf("'delegated_identity' is required") |
| 123 } |
| 124 if req.DelegatedIdentity == Requestor { |
| 125 delegatee = requestor // the requestor is delegating its own ide
ntity |
| 126 } else { |
| 127 if delegatee, err = identity.MakeIdentity(req.DelegatedIdentity)
; err != nil { |
| 128 return nil, fmt.Errorf("bad 'delegated_identity' - %s",
err) |
| 129 } |
| 130 } |
| 131 |
| 132 // Validate 'audience', convert it into a set. |
| 133 if len(req.Audience) == 0 { |
| 134 return nil, fmt.Errorf("'audience' is required") |
| 135 } |
| 136 audienceSet, err := identityset.FromStrings(req.Audience, skipRequestor) |
| 137 if err != nil { |
| 138 return nil, fmt.Errorf("bad 'audience' - %s", err) |
| 139 } |
| 140 if sliceHasString(req.Audience, Requestor) { |
| 141 audienceSet.AddIdentity(requestor) |
| 142 } |
| 143 |
| 144 // Split 'services' into two lists: URLs and everything else (which is |
| 145 // "service:..." and "*" presumably, validated below). |
| 146 if len(req.Services) == 0 { |
| 147 return nil, fmt.Errorf("'services' is required") |
| 148 } |
| 149 urls := make([]string, 0, len(req.Services)) |
| 150 rest := make([]string, 0, len(req.Services)) |
| 151 for _, srv := range req.Services { |
| 152 if strings.HasPrefix(srv, "https://") { |
| 153 urls = append(urls, srv) |
| 154 } else { |
| 155 rest = append(rest, srv) |
| 156 } |
| 157 } |
| 158 |
| 159 // Convert the list into a set, verify it contains only services (or "*"
). |
| 160 servicesSet, err := identityset.FromStrings(rest, nil) |
| 161 if err != nil { |
| 162 return nil, fmt.Errorf("bad 'services' - %s", err) |
| 163 } |
| 164 if len(servicesSet.Groups) != 0 { |
| 165 return nil, fmt.Errorf("bad 'services' - can't specify groups") |
| 166 } |
| 167 for ident := range servicesSet.IDs { |
| 168 if ident.Kind() != identity.Service { |
| 169 return nil, fmt.Errorf("bad 'services' - %q is not a ser
vice ID", ident) |
| 170 } |
| 171 } |
| 172 |
| 173 // Resolve URLs into app IDs. This may involve URL fetch calls (if the c
ache |
| 174 // is cold), so skip this expensive call if already specifying the unive
rsal |
| 175 // set of all services. |
| 176 if !servicesSet.All && len(urls) != 0 { |
| 177 if err = resolveServiceIDs(c, urls, servicesSet); err != nil { |
| 178 return nil, err |
| 179 } |
| 180 } |
| 181 |
| 182 // Done! |
| 183 return &RulesQuery{ |
| 184 Requestor: requestor, |
| 185 Delegatee: delegatee, |
| 186 Audience: audienceSet, |
| 187 Services: servicesSet, |
| 188 }, nil |
| 189 } |
| 190 |
| 191 // resolveServiceIDs takes a bunch of service URLs and resolves them to |
| 192 // 'service:<app-id>' identities, putting them in the 'out' set. |
| 193 // |
| 194 // May return transient errors. |
| 195 func resolveServiceIDs(c context.Context, urls []string, out *identityset.Set) e
rror { |
| 196 // URL fetch calls below should be extra fast. If they get stuck, someth
ing is |
| 197 // horribly wrong, better to abort soon. |
| 198 c, abort := context.WithTimeout(c, 5*time.Second) |
| 199 defer abort() |
| 200 |
| 201 type Result struct { |
| 202 URL string |
| 203 ID identity.Identity |
| 204 Err error |
| 205 } |
| 206 |
| 207 ch := make(chan Result, len(urls)) |
| 208 |
| 209 for _, url := range urls { |
| 210 go func(url string) { |
| 211 id, err := signing.FetchLUCIServiceIdentity(c, url) |
| 212 ch <- Result{url, id, err} |
| 213 }(url) |
| 214 } |
| 215 |
| 216 for i := 0; i < len(urls); i++ { |
| 217 result := <-ch |
| 218 if result.Err != nil { |
| 219 if errors.IsTransient(result.Err) { |
| 220 return result.Err |
| 221 } |
| 222 return fmt.Errorf("could not resolve %q to service ID -
%s", result.URL, result.Err) |
| 223 } |
| 224 out.AddIdentity(result.ID) |
| 225 } |
| 226 |
| 227 return nil |
| 228 } |
| OLD | NEW |