When IsAuthorizedRequest carries KeyId instead of Key, the server skips the secret check and validates only that the key exists, is unexpired, and belongs to an active, non-deleted, non-system user. This is the path used by in-process delegated callers (e.g., chatd) that hold only the key ID.
(t *testing.T)
| 224 | // belongs to an active, non-deleted, non-system user. This is the path used by |
| 225 | // in-process delegated callers (e.g., chatd) that hold only the key ID. |
| 226 | func TestAuthorization_Delegated(t *testing.T) { |
| 227 | t.Parallel() |
| 228 | |
| 229 | cases := []struct { |
| 230 | name string |
| 231 | mocksFn func(db *dbmock.MockStore, apiKey database.APIKey, user database.User) |
| 232 | bothFields bool |
| 233 | expectedErr error |
| 234 | }{ |
| 235 | { |
| 236 | name: "valid", |
| 237 | mocksFn: func(db *dbmock.MockStore, apiKey database.APIKey, user database.User) { |
| 238 | db.EXPECT().GetAPIKeyByID(gomock.Any(), apiKey.ID).Times(1).Return(apiKey, nil) |
| 239 | db.EXPECT().GetUserByID(gomock.Any(), user.ID).Times(1).Return(user, nil) |
| 240 | }, |
| 241 | }, |
| 242 | { |
| 243 | name: "unknown key", |
| 244 | expectedErr: aibridgedserver.ErrUnknownKey, |
| 245 | mocksFn: func(db *dbmock.MockStore, apiKey database.APIKey, _ database.User) { |
| 246 | db.EXPECT().GetAPIKeyByID(gomock.Any(), apiKey.ID).Times(1).Return(database.APIKey{}, sql.ErrNoRows) |
| 247 | }, |
| 248 | }, |
| 249 | { |
| 250 | name: "expired", |
| 251 | expectedErr: aibridgedserver.ErrExpired, |
| 252 | mocksFn: func(db *dbmock.MockStore, apiKey database.APIKey, _ database.User) { |
| 253 | apiKey.ExpiresAt = dbtime.Now().Add(-time.Hour) |
| 254 | db.EXPECT().GetAPIKeyByID(gomock.Any(), apiKey.ID).Times(1).Return(apiKey, nil) |
| 255 | }, |
| 256 | }, |
| 257 | { |
| 258 | // Sending both Key and KeyId is an API misuse and must be |
| 259 | // rejected to avoid ambiguity about which path was taken. |
| 260 | name: "both fields set", |
| 261 | bothFields: true, |
| 262 | expectedErr: aibridgedserver.ErrAmbiguousAuth, |
| 263 | }, |
| 264 | { |
| 265 | // A bogus secret has no effect on the delegated path because |
| 266 | // the secret is never checked. This is the load-bearing |
| 267 | // security property: trust is established out-of-band, not in |
| 268 | // this RPC. |
| 269 | name: "secret hash mismatch is ignored", |
| 270 | mocksFn: func(db *dbmock.MockStore, apiKey database.APIKey, user database.User) { |
| 271 | apiKey.HashedSecret = []byte("not-the-real-hash") |
| 272 | db.EXPECT().GetAPIKeyByID(gomock.Any(), apiKey.ID).Times(1).Return(apiKey, nil) |
| 273 | db.EXPECT().GetUserByID(gomock.Any(), user.ID).Times(1).Return(user, nil) |
| 274 | }, |
| 275 | }, |
| 276 | { |
| 277 | // The delegated path must still reject keys whose owner has |
| 278 | // been deleted; trust at the transport boundary does not |
| 279 | // extend to bypassing user-status checks. |
| 280 | name: "deleted user", |
| 281 | expectedErr: aibridgedserver.ErrDeletedUser, |
| 282 | mocksFn: func(db *dbmock.MockStore, apiKey database.APIKey, user database.User) { |
| 283 | user.Deleted = true |
nothing calls this directly
no test coverage detected