Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,35 @@ func (app *ApiServer) recoverAuthorityFromSignatureHeaders(c *fiber.Ctx) (int32,
return userId, walletLower
}

// Checks if authedWallet is authorized to act on behalf of userId
func (app *ApiServer) isAuthorizedRequest(c *fiber.Ctx, userId int32, authedWallet string) bool {
cacheKey := fmt.Sprintf("%d:%s", userId, authedWallet)
if hit, ok := app.resolveGrantCache.Get(cacheKey); ok {
return hit
}

var isAuthorized bool
err := app.pool.QueryRow(c.Context(), `
SELECT EXISTS (
SELECT 1
FROM grants
WHERE
is_current = true
AND user_id = $1
AND grantee_address = $2
AND is_approved = true
AND is_revoked = false
)
`, userId, authedWallet).Scan(&isAuthorized)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this prone to any casing issues or did we fix that already?


if err != nil {
return false
}

app.resolveGrantCache.Set(cacheKey, isAuthorized)
return isAuthorized
}

func (app *ApiServer) getAuthedUserId(c *fiber.Ctx) int32 {
return int32(c.Locals("authedUserId").(int32))
}
Expand All @@ -78,11 +107,28 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
return c.Next()
}

// Middleware that asserts authedUserId is valid
func (app *ApiServer) requiresAuthMiddleware(c *fiber.Ctx) error {
// Middleware that asserts the authedUserId is valid and is the same as the userId in
// the request path or a managed user of the authedUserId
func (app *ApiServer) requireAuthMiddleware(c *fiber.Ctx) error {
authedUserId := app.getAuthedUserId(c)
authedWallet := app.getAuthedWallet(c)
myId := app.getMyId(c)
wallet := c.Params("wallet")
if authedUserId == 0 {
return fiber.NewError(fiber.StatusUnauthorized, "You must be logged in to make this request")
}
return c.Next()

if myId != 0 && myId == authedUserId {
return c.Next()
}

if wallet != "" && wallet == authedWallet {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this meant as a catch-all for endpoints that use a wallet param somewhere in their URL?

return c.Next()
}

if app.isAuthorizedRequest(c, myId, authedWallet) {
return c.Next()
}

return fiber.NewError(fiber.StatusForbidden, "You are not authorized to make this request")
}
50 changes: 50 additions & 0 deletions api/auth_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,53 @@ func TestRecoverAuthorityFromSignatureHeaders(t *testing.T) {
assert.Equal(t, int32(1), userId)
assert.Equal(t, "0x7d273271690538cf855e5b3002a0dd8c154bb060", wallet)
}

func TestRequireAuthMiddleware(t *testing.T) {
// Create a dummy endpoint to test the requireAuthMiddleware
testApp := fiber.New()
testApp.Get("/", app.resolveMyIdMiddleware, app.authMiddleware, app.requireAuthMiddleware, func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})

// Unauthorized when no auth headers
req1 := httptest.NewRequest("GET", "/", nil)
res, err := testApp.Test(req1, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)

// Forbidden when not authorized
req2 := httptest.NewRequest("GET", "/?user_id=1", nil)
// wallet: 0x681c616ae836ceca1effe00bd07f2fdbf9a082bc
req2.Header.Set("Encoded-Data-Message", "signature:1745543704165")
req2.Header.Set("Encoded-Data-Signature", "0x4af765948dccd72026f1059a59c7a6a1172628255d7d387d1590c0fe43961c5908fc6011443805ca0dbd39156300c04dc21bbfa9adce50acea9ad29a7e2fde2a1b")
res, err = testApp.Test(req2, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusForbidden, res.StatusCode)

// Forbidden when grant is revoked
req3 := httptest.NewRequest("GET", "/?user_id=1", nil)
// wallet: 0xc451c1f8943b575158310552b41230c61844a1c1
req3.Header.Set("Encoded-Data-Message", "signature:1745542789211")
req3.Header.Set("Encoded-Data-Signature", "0xffd5f92c0d253c7222cd407cf3398fac664530ef968bd4435ea698ba1daee1d73353330848b65d212eeeaae9f41e177e49078c4efa1131e5e517090626f6dd961c")
res, err = testApp.Test(req3, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusForbidden, res.StatusCode)

// Authorized when grant is approved
req4 := httptest.NewRequest("GET", "/?user_id=1", nil)
// wallet: 0x5f1a372b28956c8363f8bc3a231a6e9e1186ead8
req4.Header.Set("Encoded-Data-Message", "signature:1745544459796")
req4.Header.Set("Encoded-Data-Signature", "0x1c9cb405d8437d28ff5596918551f7a45f981e81618d65ee10892313292a8c7a325af002231d115b28ca2d244b082abe1bde4a7d9610f8140d3738a9be5c4fd91b")
res, err = testApp.Test(req4, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusOK, res.StatusCode)

// Authorized when own user
req5 := httptest.NewRequest("GET", "/?user_id=1", nil)
// wallet: 0x7d273271690538cf855e5b3002a0dd8c154bb060
req5.Header.Set("Encoded-Data-Message", "signature:1744763856446")
req5.Header.Set("Encoded-Data-Signature", "0xbb202be3a7f3a0aa22c1458ef6a3f2f8360fb86791c7b137e8562df0707825c11fa1db01096efd2abc5e6613c4d1e8d4ae1e2b993abdd555fe270c1b17bff0d21c")
res, err = testApp.Test(req5, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusOK, res.StatusCode)
}
13 changes: 13 additions & 0 deletions api/fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ var (
"splits": "[]",
"created_at": time.Now(),
}

grantBaseRow = map[string]any{
"blockhash": "block1",
"blocknumber": 101,
"user_id": nil,
"is_current": true,
"grantee_address": nil,
"is_approved": false,
"is_revoked": false,
"created_at": time.Now(),
"updated_at": time.Now(),
"txhash": "tx123",
}
)

func insertFixtures(table string, baseRow map[string]any, csvFile string) {
Expand Down
6 changes: 5 additions & 1 deletion api/resolve_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ func (app *ApiServer) resolveMyIdMiddleware(c *fiber.Ctx) error {
}

func (app *ApiServer) getMyId(c *fiber.Ctx) int32 {
return int32(c.Locals("myId").(int))
myId := c.Locals("myId")
if myId == nil {
return 0
}
return int32(myId.(int))
}

func (app *ApiServer) requireUserIdMiddleware(c *fiber.Ctx) error {
Expand Down
15 changes: 14 additions & 1 deletion api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ func NewApiServer(config config.Config) *ApiServer {
}

pool, err := pgxpool.NewWithConfig(context.Background(), connConfig)
// To turn off pgx logging, use this:
// pool, err := pgxpool.New(context.Background(), config.DbUrl)

if err != nil {
logger.Fatal("db connect failed", zap.Error(err))
}
Expand All @@ -100,6 +103,14 @@ func NewApiServer(config config.Config) *ApiServer {
panic(err)
}

resolveGrantCache, err := otter.MustBuilder[string, bool](50_000).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I wrong or does this mean when you delete a grant we will take 10 minutes to stop honoring it for the manager/app in question?
Seems like that might create a concerning UX or folks who expect the revocation to be immediate.

WithTTL(10 * time.Minute).
CollectStats().
Build()
if err != nil {
panic(err)
}

privateKey, err := crypto.HexToECDSA(config.DelegatePrivateKey)
if err != nil {
panic(err)
Expand All @@ -117,6 +128,7 @@ func NewApiServer(config config.Config) *ApiServer {
started: time.Now(),
resolveHandleCache: resolveHandleCache,
resolveWalletCache: resolveWalletCache,
resolveGrantCache: resolveGrantCache,
rewardAttester: *rewards.NewRewardAttester(privateKey, []rewards.Reward{}),
solanaConfig: config.SolanaConfig,
antiAbuseOracles: config.AntiAbuseOracles,
Expand Down Expand Up @@ -183,7 +195,7 @@ func NewApiServer(config config.Config) *ApiServer {
// Users
g.Get("/users", app.v1Users)

g.Get("/users/account/:wallet", app.requiresAuthMiddleware, app.v1UsersAccount)
g.Get("/users/account/:wallet", app.requireAuthMiddleware, app.v1UsersAccount)

g.Use("/users/handle/:handle", app.requireHandleMiddleware)
g.Get("/users/handle/:handle", app.v1User)
Expand Down Expand Up @@ -255,6 +267,7 @@ type ApiServer struct {
started time.Time
resolveHandleCache otter.Cache[string, int32]
resolveWalletCache otter.Cache[string, int32]
resolveGrantCache otter.Cache[string, bool]
rewardAttester rewards.RewardAttester
solanaConfig config.SolanaConfig
antiAbuseOracles []string
Expand Down
1 change: 1 addition & 0 deletions api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func TestMain(m *testing.M) {
insertFixtures("aggregate_user_tips", aggregateUserTipsBaseRow, "testdata/aggregate_user_tips_fixtures.csv")
insertFixtures("usdc_purchases", usdcPurchaseBaseRow, "testdata/usdc_purchases_fixtures.csv")
insertFixtures("track_routes", map[string]any{}, "testdata/track_routes_fixtures.csv")
insertFixtures("grants", grantBaseRow, "testdata/grants_fixtures.csv")

// index to es / os

Expand Down
3 changes: 3 additions & 0 deletions api/testdata/grants_fixtures.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
user_id,grantee_address,is_approved,is_revoked
1,0x5f1a372b28956c8363f8bc3a231a6e9e1186ead8,true,false
1,0xc451c1f8943b575158310552b41230c61844a1c1,false,true
3 changes: 3 additions & 0 deletions api/testdata/user_fixtures.csv
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ user_id,handle,handle_lc,is_deactivated,wallet,playlist_library
5,guyintrending,guyintrending,f,0x34567890abcdef13,
6,TracksByPermalink,tracksbypermalink,f,0xffffffffff,
91,badguy,badguy,t,0x4567890abcdef123,
100,authtest1,authtest1,f,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc,
101,authtest2,authtest2,f,0xc451c1f8943b575158310552b41230c61844a1c1,
102,authtest3,authtest3,f,0x5f1a372b28956c8363f8bc3a231a6e9e1186ead8,
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
Expand Down
2 changes: 1 addition & 1 deletion static/apidiff.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"/v1/users/PWgX8NR/followers?limit=15&offset=0&user_id=aNzoj",
"/v1/users/PWgX8NR/following?limit=15&offset=0&user_id=aNzoj",
"/v1/users/PWgX8NR/mutuals?limit=5&offset=0&user_id=aNzoj",

"/v1/developer_apps/7d7b6b7a97d1deefe3a1ccc5a13c48e8f055e0b6",
"/v1/full/users/account/0x7d273271690538cf855e5b3002a0dd8c154bb060"
];

const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
Expand Down