diff --git a/scnserver/TODO.md b/scnserver/TODO.md index e6ed3ec..852daf6 100644 --- a/scnserver/TODO.md +++ b/scnserver/TODO.md @@ -49,7 +49,9 @@ - We no longer have a route to reshuffle all keys (previously in updateUser), add a /user/:uid/keys/reset ? Would delete all existing keys and create 3 new ones? -- TODO-comments + - TODO-comments + + - why do some tests take 5 seconds (= duration of context timeout??) #### PERSONAL diff --git a/scnserver/api/handler/api.go b/scnserver/api/handler/api.go index f0f4c4a..01d598f 100644 --- a/scnserver/api/handler/api.go +++ b/scnserver/api/handler/api.go @@ -607,7 +607,7 @@ func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse { return *permResp } - channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) + channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } @@ -753,7 +753,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { return *permResp } - oldChannel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) + oldChannel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } @@ -816,7 +816,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse { } - channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) + channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err) } @@ -876,7 +876,7 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse { pageSize := mathext.Clamp(langext.Coalesce(q.PageSize, 64), 1, maxPageSize) - channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID) + channel, err := h.database.GetChannel(ctx, u.ChannelUserID, u.ChannelID, false) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } @@ -1052,7 +1052,7 @@ func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPRespons return *permResp } - _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) + _, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID, true) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) } diff --git a/scnserver/db/impl/primary/channels.go b/scnserver/db/impl/primary/channels.go index 8f7768e..0e559ec 100644 --- a/scnserver/db/impl/primary/channels.go +++ b/scnserver/db/impl/primary/channels.go @@ -176,17 +176,28 @@ func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, co return data, nil } -func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid models.ChannelID) (models.ChannelWithSubscription, error) { +func (db *Database) GetChannel(ctx TxContext, userid models.UserID, channelid models.ChannelID, enforceOwner bool) (models.ChannelWithSubscription, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { return models.ChannelWithSubscription{}, err } - rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid AND channels.channel_id = :cid LIMIT 1", sq.PP{ - "ouid": userid, + params := sq.PP{ "cid": channelid, "subuid": userid, - }) + } + + selectors := "channels.*, sub.*" + + join := "LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid" + + cond := "channels.channel_id = :cid" + if enforceOwner { + cond = "owner_user_id = :ouid AND channels.channel_id = :cid" + params["ouid"] = userid + } + + rows, err := tx.Query(ctx, "SELECT "+selectors+" FROM channels "+join+" WHERE "+cond+" LIMIT 1", params) if err != nil { return models.ChannelWithSubscription{}, err } diff --git a/scnserver/logic/permissions.go b/scnserver/logic/permissions.go index 47e9254..7ab4e40 100644 --- a/scnserver/logic/permissions.go +++ b/scnserver/logic/permissions.go @@ -49,8 +49,11 @@ func (ac *AppContext) CheckPermissionChanMessagesRead(channel models.Channel) *g if err != nil { return langext.Ptr(ginresp.APIError(ac.ginContext, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)) } + if sub == nil { + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action (no subscription)", nil)) + } if !sub.Confirmed { - return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)) + return langext.Ptr(ginresp.APIError(ac.ginContext, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action (subscription not confirmed)", nil)) } return nil // subscribed channel } diff --git a/scnserver/test/channel_test.go b/scnserver/test/channel_test.go index 834cead..adfd8eb 100644 --- a/scnserver/test/channel_test.go +++ b/scnserver/test/channel_test.go @@ -848,9 +848,265 @@ func TestListChannelSubscriptions(t *testing.T) { 0, 0, 0, 0, 0, 0, 0, 0, 0) - } -//TODO test list messages of unsubscribed channel -//TODO test list messages of sub-not-confirmed channel -//TODO test list messages of sub-deleted channel +func TestListChannelMessagesOfUnsubscribed(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data1 := tt.InitSingleData(t, ws) + data2 := tt.InitSingleData(t, ws) + + type msg struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + Content string `json:"content"` + MessageId string `json:"message_id"` + OwnerUserId string `json:"owner_user_id"` + Priority int `json:"priority"` + SenderIp string `json:"sender_ip"` + SenderName string `json:"sender_name"` + SenderUserId string `json:"sender_user_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Trimmed bool `json:"trimmed"` + UsrMessageId string `json:"usr_message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + NPT string `json:"next_page_token"` + PageSize int `json:"page_size"` + } + + type chanobj struct { + ChannelId string `json:"channel_id"` + DescriptionName string `json:"description_name"` + DisplayName string `json:"display_name"` + InternalName string `json:"internal_name"` + MessagesSent int `json:"messages_sent"` + OwnerUserId string `json:"owner_user_id"` + SubscribeKey string `json:"subscribe_key"` + Subscription struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + ChannelOwnerUserId string `json:"channel_owner_user_id"` + Confirmed bool `json:"confirmed"` + SubscriberUserId string `json:"subscriber_user_id"` + SubscriptionId string `json:"subscription_id"` + TimestampCreated string `json:"timestamp_created"` + } `json:"subscription"` + TimestampCreated string `json:"timestamp_created"` + TimestampLastsent string `json:"timestamp_lastsent"` + } + + type chanlist struct { + Channels []chanobj `json:"channels"` + } + + chan1 := tt.RequestAuthPost[chanobj](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data2.UserID), gin.H{ + "name": "chan1", + }) + + tt.RequestAuthGetShouldFail(t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", data1.UserID, chan1.ChannelId), 401, apierr.USER_AUTH_FAILED) +} + +func TestListChannelMessagesOfUnconfirmed1(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data1 := tt.InitSingleData(t, ws) + data2 := tt.InitSingleData(t, ws) + + type msg struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + Content string `json:"content"` + MessageId string `json:"message_id"` + OwnerUserId string `json:"owner_user_id"` + Priority int `json:"priority"` + SenderIp string `json:"sender_ip"` + SenderName string `json:"sender_name"` + SenderUserId string `json:"sender_user_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Trimmed bool `json:"trimmed"` + UsrMessageId string `json:"usr_message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + NPT string `json:"next_page_token"` + PageSize int `json:"page_size"` + } + + type chanobj struct { + ChannelId string `json:"channel_id"` + DescriptionName string `json:"description_name"` + DisplayName string `json:"display_name"` + InternalName string `json:"internal_name"` + MessagesSent int `json:"messages_sent"` + OwnerUserId string `json:"owner_user_id"` + SubscribeKey string `json:"subscribe_key"` + Subscription struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + ChannelOwnerUserId string `json:"channel_owner_user_id"` + Confirmed bool `json:"confirmed"` + SubscriberUserId string `json:"subscriber_user_id"` + SubscriptionId string `json:"subscription_id"` + TimestampCreated string `json:"timestamp_created"` + } `json:"subscription"` + TimestampCreated string `json:"timestamp_created"` + TimestampLastsent string `json:"timestamp_lastsent"` + } + + type chanlist struct { + Channels []chanobj `json:"channels"` + } + + chan1 := tt.RequestAuthPost[chanobj](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data2.UserID), gin.H{ + "name": "chan1", + }) + + tt.RequestAuthPost[gin.H](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?chan_subscribe_key=%s", data1.UserID, chan1.SubscribeKey), gin.H{ + "channel_owner_user_id": data2.UserID, + "channel_internal_name": "chan1", + }) + + tt.RequestAuthGetShouldFail(t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", data1.UserID, chan1.ChannelId), 401, apierr.USER_AUTH_FAILED) +} + +func TestListChannelMessagesOfUnconfirmed2(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data1 := tt.InitSingleData(t, ws) + data2 := tt.InitSingleData(t, ws) + + type msg struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + Content string `json:"content"` + MessageId string `json:"message_id"` + OwnerUserId string `json:"owner_user_id"` + Priority int `json:"priority"` + SenderIp string `json:"sender_ip"` + SenderName string `json:"sender_name"` + SenderUserId string `json:"sender_user_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Trimmed bool `json:"trimmed"` + UsrMessageId string `json:"usr_message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + NPT string `json:"next_page_token"` + PageSize int `json:"page_size"` + } + + type chanobj struct { + ChannelId string `json:"channel_id"` + DescriptionName string `json:"description_name"` + DisplayName string `json:"display_name"` + InternalName string `json:"internal_name"` + MessagesSent int `json:"messages_sent"` + OwnerUserId string `json:"owner_user_id"` + SubscribeKey string `json:"subscribe_key"` + Subscription struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + ChannelOwnerUserId string `json:"channel_owner_user_id"` + Confirmed bool `json:"confirmed"` + SubscriberUserId string `json:"subscriber_user_id"` + SubscriptionId string `json:"subscription_id"` + TimestampCreated string `json:"timestamp_created"` + } `json:"subscription"` + TimestampCreated string `json:"timestamp_created"` + TimestampLastsent string `json:"timestamp_lastsent"` + } + + type chanlist struct { + Channels []chanobj `json:"channels"` + } + + chan1 := tt.RequestAuthPost[chanobj](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data2.UserID), gin.H{ + "name": "chan1", + }) + + tt.RequestAuthPost[gin.H](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?chan_subscribe_key=%s", data1.UserID, chan1.SubscribeKey), gin.H{ + "channel_id": chan1.ChannelId, + }) + + tt.RequestAuthGetShouldFail(t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", data1.UserID, chan1.ChannelId), 401, apierr.USER_AUTH_FAILED) +} + +func TestListChannelMessagesOfRevokedConfirmation(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data1 := tt.InitSingleData(t, ws) + data2 := tt.InitSingleData(t, ws) + + type msg struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + Content string `json:"content"` + MessageId string `json:"message_id"` + OwnerUserId string `json:"owner_user_id"` + Priority int `json:"priority"` + SenderIp string `json:"sender_ip"` + SenderName string `json:"sender_name"` + SenderUserId string `json:"sender_user_id"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + Trimmed bool `json:"trimmed"` + UsrMessageId string `json:"usr_message_id"` + } + type mglist struct { + Messages []msg `json:"messages"` + NPT string `json:"next_page_token"` + PageSize int `json:"page_size"` + } + + type chanobj struct { + ChannelId string `json:"channel_id"` + DescriptionName string `json:"description_name"` + DisplayName string `json:"display_name"` + InternalName string `json:"internal_name"` + MessagesSent int `json:"messages_sent"` + OwnerUserId string `json:"owner_user_id"` + SubscribeKey string `json:"subscribe_key"` + Subscription struct { + ChannelId string `json:"channel_id"` + ChannelInternalName string `json:"channel_internal_name"` + ChannelOwnerUserId string `json:"channel_owner_user_id"` + Confirmed bool `json:"confirmed"` + SubscriberUserId string `json:"subscriber_user_id"` + SubscriptionId string `json:"subscription_id"` + TimestampCreated string `json:"timestamp_created"` + } `json:"subscription"` + TimestampCreated string `json:"timestamp_created"` + TimestampLastsent string `json:"timestamp_lastsent"` + } + + type chanlist struct { + Channels []chanobj `json:"channels"` + } + + chan1 := tt.RequestAuthPost[chanobj](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data2.UserID), gin.H{ + "name": "chan1", + }) + + sub1 := tt.RequestAuthPost[gin.H](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?chan_subscribe_key=%s", data1.UserID, chan1.SubscribeKey), gin.H{ + "channel_id": chan1.ChannelId, + }) + + tt.RequestAuthPatch[gin.H](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data2.UserID, sub1["subscription_id"]), gin.H{ + "confirmed": true, + }) + + tt.RequestAuthGet[tt.Void](t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", data1.UserID, chan1.ChannelId)) + + tt.RequestAuthDelete[gin.H](t, data2.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data2.UserID, sub1["subscription_id"]), gin.H{}) + + tt.RequestAuthGetShouldFail(t, data1.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s/messages", data1.UserID, chan1.ChannelId), 401, apierr.USER_AUTH_FAILED) +}