From a180c74c3819cf865b900c35a0d0993c5f4af2c8 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 17 Mar 2021 10:46:05 +0100 Subject: [PATCH] begin sign-in with ssb * sketch session store * use session store and unify authentication handling * sketch muxrpc handlers --- cmd/server/main.go | 2 + go.mod | 5 +- go.sum | 11 +- internal/network/interface.go | 8 +- internal/network/mocked/endpoints.go | 118 ++ internal/randutil/string.go | 16 + internal/signinwithssb/challenges.go | 66 ++ internal/signinwithssb/simple_test.go | 29 + muxrpc/handlers/signinwithssb/withssb.go | 102 ++ roomdb/interface.go | 18 +- roomdb/mockdb/auth.go | 239 ++++ roomdb/mockdb/members.go | 78 -- roomdb/sqlite/auth_withssb.go | 113 ++ roomdb/sqlite/invites.go | 10 +- roomdb/sqlite/migrations/03-siwssb-tokens.sql | 17 + roomdb/sqlite/models/SIWSSB_sessions.go | 1032 +++++++++++++++++ roomdb/sqlite/models/aliases.go | 46 - roomdb/sqlite/models/boil_table_names.go | 2 + roomdb/sqlite/models/denied_keys.go | 21 - roomdb/sqlite/models/members.go | 175 +++ roomdb/sqlite/new.go | 5 +- roomsrv/init_handlers.go | 10 + web/errors/badrequest.go | 11 + web/handlers/admin/setup_test.go | 16 +- web/handlers/auth/handler.go | 13 +- web/handlers/auth/withssb.go | 255 ++++ web/handlers/auth_test.go | 203 +++- web/handlers/http.go | 34 +- web/handlers/invites.go | 3 +- web/handlers/setup_test.go | 9 + web/i18n/defaults/active.en.toml | 3 + web/members/helper.go | 63 +- web/members/testing.go | 2 +- web/router/auth.go | 10 +- web/templates/auth/withssb_sign_in.tmpl | 9 + web/templates/base.tmpl | 2 +- 36 files changed, 2537 insertions(+), 219 deletions(-) create mode 100644 internal/network/mocked/endpoints.go create mode 100644 internal/randutil/string.go create mode 100644 internal/signinwithssb/challenges.go create mode 100644 internal/signinwithssb/simple_test.go create mode 100644 muxrpc/handlers/signinwithssb/withssb.go create mode 100644 roomdb/sqlite/migrations/03-siwssb-tokens.sql create mode 100644 roomdb/sqlite/models/SIWSSB_sessions.go create mode 100644 web/handlers/auth/withssb.go create mode 100644 web/templates/auth/withssb_sign_in.tmpl diff --git a/cmd/server/main.go b/cmd/server/main.go index e84b106..4bd0043 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -248,9 +248,11 @@ func runroomsrv() error { RoomID: roomsrv.Whoami(), }, roomsrv.StateManager, + roomsrv.Network, handlers.Databases{ Aliases: db.Aliases, AuthFallback: db.AuthFallback, + AuthWithSSB: db.AuthWithSSB, DeniedKeys: db.DeniedKeys, Invites: db.Invites, Notices: db.Notices, diff --git a/go.mod b/go.mod index 233d69f..778cd1f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/BurntSushi/toml v0.3.1 github.com/PuerkitoBio/goquery v1.5.0 - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.0 github.com/friendsofgo/errors v0.9.2 github.com/go-kit/kit v0.10.0 github.com/gofrs/uuid v4.0.0+incompatible // indirect @@ -15,7 +15,6 @@ require ( github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.4.2 github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472 - github.com/kevinburke/go-bindata v3.21.0+incompatible // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/nicksnyder/go-i18n/v2 v2.1.2 github.com/pkg/errors v0.9.1 @@ -30,7 +29,7 @@ require ( go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59 go.cryptoscope.co/netwrap v0.1.1 go.cryptoscope.co/secretstream v1.2.2 - go.mindeco.de v1.8.0 + go.mindeco.de v1.9.0 go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 diff --git a/go.sum b/go.sum index 0c29012..f9a0e76 100644 --- a/go.sum +++ b/go.sum @@ -232,7 +232,6 @@ github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472 h1:6nrO82kszcc+rcKP github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472/go.mod h1:oLLUlGld/axGHThR36o8bADQUHG+TKSUdoKqCvnoQB4= github.com/keks/persist v0.0.0-20180731151133-9546f7b3f97e/go.mod h1:KMIOJFEE+0E/mYfYExA9vOpCFDz4TQfzk6mCOtCXR9k= github.com/keks/persist v0.0.0-20181029214439-3af502dad70b/go.mod h1:KMIOJFEE+0E/mYfYExA9vOpCFDz4TQfzk6mCOtCXR9k= -github.com/kevinburke/go-bindata v3.21.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -427,18 +426,14 @@ github.com/vcraescu/go-paginator/v2 v2.0.0 h1:m9If0wF7pSjYfocrJZcyWNiWn7OfIeLFVQ github.com/vcraescu/go-paginator/v2 v2.0.0/go.mod h1:qsrC8+/YgRL0LfurxeY3gCAtsN7oOthkIbmBdqpMX9U= github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU= github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA= -github.com/volatiletech/null/v8 v8.1.0 h1:eAO3I31A5R04usY5SKMMfDcOCnEGyT/T4wRI0JVGp4U= github.com/volatiletech/null/v8 v8.1.0/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g= +github.com/volatiletech/null/v8 v8.1.2 h1:kiTiX1PpwvuugKwfvUNX/SU/5A2KGZMXfGD0DUHdKEI= github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g= github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5sbWC4nMk= github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY= -github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14 h1:2PCMsnM/GVptZVyB8s0vTIFCPjl6f5rhz5Ry5MNShMQ= -github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw= github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef h1:XjoYLjR/XToxGQY9O6WKA8l6t0hIWjPwazVQcQEctFM= github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw= github.com/volatiletech/sqlboiler/v4 v4.0.0/go.mod h1:U0Z5K4y+twWgHxh364G45QyzyNssSbBqNWtXGHVTlgM= -github.com/volatiletech/sqlboiler/v4 v4.4.0 h1:aSlvHidRBuxHHQZNX3ZLGgzNVPVPzWqsC3lhcLbV/b0= -github.com/volatiletech/sqlboiler/v4 v4.4.0/go.mod h1:h4RBAO6QbwMP3ezGmtfGljRms7S27cFIgF3rKgPKstE= github.com/volatiletech/sqlboiler/v4 v4.5.0 h1:oJ3YXEvv0c48S9W/3TuPLxJxefIkewpub2qZioXXlUY= github.com/volatiletech/sqlboiler/v4 v4.5.0/go.mod h1:tQgF5zxwqrjR6Wydc5rRylI6puDOO1WvBC70/5up+Hg= github.com/volatiletech/strmangle v0.0.1 h1:UKQoHmY6be/R3tSvD2nQYrH41k43OJkidwEiC74KIzk= @@ -473,8 +468,8 @@ go.cryptoscope.co/secretstream v1.2.2/go.mod h1:7nRGZ7fTqSgQAnv2Y4m8xQsS3MFxvB7I go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.mindeco.de v1.8.0 h1:Vxob3XaDz85aD4wq8VbQxtradpHbmjciG2eSJLGaFV0= -go.mindeco.de v1.8.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE= +go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg= +go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE= go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c= go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= diff --git a/internal/network/interface.go b/internal/network/interface.go index fa8db20..d45c8da 100644 --- a/internal/network/interface.go +++ b/internal/network/interface.go @@ -24,13 +24,19 @@ type EndpointStat struct { Endpoint muxrpc.Endpoint } +//go:generate counterfeiter -o mocked/endpoints.go . Endpoints + +type Endpoints interface { + GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool) +} + type Network interface { Connect(ctx context.Context, addr net.Addr) error Serve(context.Context, ...muxrpc.HandlerWrapper) error GetListenAddr() net.Addr GetAllEndpoints() []EndpointStat - GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool) + Endpoints GetConnTracker() ConnTracker diff --git a/internal/network/mocked/endpoints.go b/internal/network/mocked/endpoints.go new file mode 100644 index 0000000..b856aec --- /dev/null +++ b/internal/network/mocked/endpoints.go @@ -0,0 +1,118 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package mocked + +import ( + "sync" + + "github.com/ssb-ngi-pointer/go-ssb-room/internal/network" + muxrpc "go.cryptoscope.co/muxrpc/v2" + refs "go.mindeco.de/ssb-refs" +) + +type FakeEndpoints struct { + GetEndpointForStub func(refs.FeedRef) (muxrpc.Endpoint, bool) + getEndpointForMutex sync.RWMutex + getEndpointForArgsForCall []struct { + arg1 refs.FeedRef + } + getEndpointForReturns struct { + result1 muxrpc.Endpoint + result2 bool + } + getEndpointForReturnsOnCall map[int]struct { + result1 muxrpc.Endpoint + result2 bool + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeEndpoints) GetEndpointFor(arg1 refs.FeedRef) (muxrpc.Endpoint, bool) { + fake.getEndpointForMutex.Lock() + ret, specificReturn := fake.getEndpointForReturnsOnCall[len(fake.getEndpointForArgsForCall)] + fake.getEndpointForArgsForCall = append(fake.getEndpointForArgsForCall, struct { + arg1 refs.FeedRef + }{arg1}) + stub := fake.GetEndpointForStub + fakeReturns := fake.getEndpointForReturns + fake.recordInvocation("GetEndpointFor", []interface{}{arg1}) + fake.getEndpointForMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeEndpoints) GetEndpointForCallCount() int { + fake.getEndpointForMutex.RLock() + defer fake.getEndpointForMutex.RUnlock() + return len(fake.getEndpointForArgsForCall) +} + +func (fake *FakeEndpoints) GetEndpointForCalls(stub func(refs.FeedRef) (muxrpc.Endpoint, bool)) { + fake.getEndpointForMutex.Lock() + defer fake.getEndpointForMutex.Unlock() + fake.GetEndpointForStub = stub +} + +func (fake *FakeEndpoints) GetEndpointForArgsForCall(i int) refs.FeedRef { + fake.getEndpointForMutex.RLock() + defer fake.getEndpointForMutex.RUnlock() + argsForCall := fake.getEndpointForArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeEndpoints) GetEndpointForReturns(result1 muxrpc.Endpoint, result2 bool) { + fake.getEndpointForMutex.Lock() + defer fake.getEndpointForMutex.Unlock() + fake.GetEndpointForStub = nil + fake.getEndpointForReturns = struct { + result1 muxrpc.Endpoint + result2 bool + }{result1, result2} +} + +func (fake *FakeEndpoints) GetEndpointForReturnsOnCall(i int, result1 muxrpc.Endpoint, result2 bool) { + fake.getEndpointForMutex.Lock() + defer fake.getEndpointForMutex.Unlock() + fake.GetEndpointForStub = nil + if fake.getEndpointForReturnsOnCall == nil { + fake.getEndpointForReturnsOnCall = make(map[int]struct { + result1 muxrpc.Endpoint + result2 bool + }) + } + fake.getEndpointForReturnsOnCall[i] = struct { + result1 muxrpc.Endpoint + result2 bool + }{result1, result2} +} + +func (fake *FakeEndpoints) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getEndpointForMutex.RLock() + defer fake.getEndpointForMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeEndpoints) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ network.Endpoints = new(FakeEndpoints) diff --git a/internal/randutil/string.go b/internal/randutil/string.go new file mode 100644 index 0000000..4a7cbf2 --- /dev/null +++ b/internal/randutil/string.go @@ -0,0 +1,16 @@ +package randutil + +import "math/rand" + +var alphabet = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +// String returns a random string of length n, using the alphnum character set (a-z, A-Z, 0-9) +func String(n int) string { + s := make([]rune, n) + + for i := range s { + s[i] = alphabet[rand.Intn(len(alphabet))] + } + + return string(s) +} diff --git a/internal/signinwithssb/challenges.go b/internal/signinwithssb/challenges.go new file mode 100644 index 0000000..559cd81 --- /dev/null +++ b/internal/signinwithssb/challenges.go @@ -0,0 +1,66 @@ +package signinwithssb + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + + "golang.org/x/crypto/ed25519" + + refs "go.mindeco.de/ssb-refs" +) + +// sign-in with ssb uses 256-bit nonces +const challengeLength = 32 + +func DecodeChallengeString(c string) ([]byte, error) { + challengeBytes, err := base64.URLEncoding.DecodeString(c) + if err != nil { + return nil, fmt.Errorf("invalid challenge encoding: %w", err) + } + + if n := len(challengeBytes); n != challengeLength { + return nil, fmt.Errorf("invalid challenge length: expected %d but got %d", challengeLength, n) + } + + return challengeBytes, nil +} + +func GenerateChallenge() string { + buf := make([]byte, challengeLength) + rand.Read(buf) + return base64.URLEncoding.EncodeToString(buf) +} + +// this structure is used to verify an incoming client response +type ClientRequest struct { + ClientID, ServerID refs.FeedRef + + ClientChallenge string + ServerChallenge string +} + +// recreate the signed message +func (cr ClientRequest) createMessage() []byte { + var msg bytes.Buffer + msg.WriteString("=http-auth-sign-in:") + msg.WriteString(cr.ServerID.Ref()) + msg.WriteString(":") + msg.WriteString(cr.ClientID.Ref()) + msg.WriteString(":") + msg.WriteString(cr.ServerChallenge) + msg.WriteString(":") + msg.WriteString(cr.ClientChallenge) + return msg.Bytes() +} + +func (cr ClientRequest) Sign(privateKey ed25519.PrivateKey) []byte { + msg := cr.createMessage() + return ed25519.Sign(privateKey, msg) +} + +func (cr ClientRequest) Validate(signature []byte) bool { + msg := cr.createMessage() + return ed25519.Verify(cr.ClientID.PubKey(), msg, signature) +} diff --git a/internal/signinwithssb/simple_test.go b/internal/signinwithssb/simple_test.go new file mode 100644 index 0000000..6dca144 --- /dev/null +++ b/internal/signinwithssb/simple_test.go @@ -0,0 +1,29 @@ +package signinwithssb + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + refs "go.mindeco.de/ssb-refs" +) + +func TestClientRequestString(t *testing.T) { + + server := refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "test"} + + client := refs.FeedRef{ID: bytes.Repeat([]byte{2}, 32), Algo: "test"} + + var req ClientRequest + + req.ServerID = server + req.ClientID = client + + req.ServerChallenge = "fooo" + req.ClientChallenge = "barr" + + want := "=http-auth-sign-in:@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=.test:@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=.test:fooo:barr" + + got := req.createMessage() + assert.Equal(t, want, string(got)) +} diff --git a/muxrpc/handlers/signinwithssb/withssb.go b/muxrpc/handlers/signinwithssb/withssb.go new file mode 100644 index 0000000..059660c --- /dev/null +++ b/muxrpc/handlers/signinwithssb/withssb.go @@ -0,0 +1,102 @@ +package signinwithssb + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + kitlog "github.com/go-kit/kit/log" + "go.cryptoscope.co/muxrpc/v2" + + "github.com/ssb-ngi-pointer/go-ssb-room/internal/network" + validate "github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + refs "go.mindeco.de/ssb-refs" +) + +// Handler implements the muxrpc methods for alias registration and recvocation +type Handler struct { + logger kitlog.Logger + self refs.FeedRef + + sessions roomdb.AuthWithSSBService + members roomdb.MembersService + + roomDomain string // the http(s) domain of the room to signal redirect addresses +} + +// New returns a fresh alias muxrpc handler +func New( + log kitlog.Logger, + self refs.FeedRef, + sessiondb roomdb.AuthWithSSBService, + membersdb roomdb.MembersService, + roomDomain string) Handler { + + var h Handler + h.self = self + h.roomDomain = roomDomain + h.logger = log + h.sessions = sessiondb + h.members = membersdb + + return h +} + +func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interface{}, error) { + clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr()) + if err != nil { + return nil, err + } + + var params []string + if err := json.Unmarshal(req.RawArgs, ¶ms); err != nil { + return nil, err + } + + if n := len(params); n != 3 { + return nil, fmt.Errorf("expected 3 arguments (sc, cc, sol) but got %d", n) + } + + var sol validate.ClientRequest + sol.ServerID = h.self + sol.ServerChallenge = params[0] + sol.ClientID = *clientID + sol.ClientChallenge = params[1] + + sig, err := base64.StdEncoding.DecodeString(params[2]) + if err != nil { + return nil, fmt.Errorf("sc is not valid base64 data: %w", err) + } + + if !sol.Validate(sig) { + return nil, fmt.Errorf("not a valid solution") + } + + // TODO: + // h.challenges.Solved(sc) + // return true, nil + + return nil, fmt.Errorf("TODO: update SSE") +} + +func (h Handler) InvalidateAllSolutions(ctx context.Context, req *muxrpc.Request) (interface{}, error) { + // get the feed from the muxrpc connection + clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr()) + if err != nil { + return nil, err + } + + member, err := h.members.GetByFeed(ctx, *clientID) + if err != nil { + return nil, err + } + + err = h.sessions.WipeTokensForMember(ctx, member.ID) + if err != nil { + return nil, err + } + + return true, nil +} diff --git a/roomdb/interface.go b/roomdb/interface.go index 55dbf8a..f1d359b 100644 --- a/roomdb/interface.go +++ b/roomdb/interface.go @@ -32,9 +32,21 @@ type AuthFallbackService interface { // Remove(pwid) } -// needed?! not sure we need to hold the challanges -// AuthWithSSBService defines functions needed for the challange/response system of sign-in with ssb -type AuthWithSSBService interface{} +// AuthWithSSBService defines utility functions for the challenge/response system of sign-in with ssb +// They are particualarly of service to check valid sessions (after the client provided a solution for a challenge) +// And to log out valid sessions from the clients device. +type AuthWithSSBService interface { + + // CreateToken is used to generate a token that is stored inside a cookie. + // It is used after a valid solution for a challenge was provided. + CreateToken(ctx context.Context, memberID int64) (string, error) + + // CheckToken checks if the passed token is still valid and returns the member id if so + CheckToken(ctx context.Context, token string) (int64, error) + + // WipeTokensForMember deletes all tokens currently held for that member + WipeTokensForMember(ctx context.Context, memberID int64) error +} // MembersService stores and retreives the list of internal users (members, mods and admins). type MembersService interface { diff --git a/roomdb/mockdb/auth.go b/roomdb/mockdb/auth.go index 81109d2..a434f2d 100644 --- a/roomdb/mockdb/auth.go +++ b/roomdb/mockdb/auth.go @@ -2,19 +2,258 @@ package mockdb import ( + "context" "sync" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" ) type FakeAuthWithSSBService struct { + CheckTokenStub func(context.Context, string) (int64, error) + checkTokenMutex sync.RWMutex + checkTokenArgsForCall []struct { + arg1 context.Context + arg2 string + } + checkTokenReturns struct { + result1 int64 + result2 error + } + checkTokenReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + CreateTokenStub func(context.Context, int64) (string, error) + createTokenMutex sync.RWMutex + createTokenArgsForCall []struct { + arg1 context.Context + arg2 int64 + } + createTokenReturns struct { + result1 string + result2 error + } + createTokenReturnsOnCall map[int]struct { + result1 string + result2 error + } + WipeTokensForMemberStub func(context.Context, int64) error + wipeTokensForMemberMutex sync.RWMutex + wipeTokensForMemberArgsForCall []struct { + arg1 context.Context + arg2 int64 + } + wipeTokensForMemberReturns struct { + result1 error + } + wipeTokensForMemberReturnsOnCall map[int]struct { + result1 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } +func (fake *FakeAuthWithSSBService) CheckToken(arg1 context.Context, arg2 string) (int64, error) { + fake.checkTokenMutex.Lock() + ret, specificReturn := fake.checkTokenReturnsOnCall[len(fake.checkTokenArgsForCall)] + fake.checkTokenArgsForCall = append(fake.checkTokenArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.CheckTokenStub + fakeReturns := fake.checkTokenReturns + fake.recordInvocation("CheckToken", []interface{}{arg1, arg2}) + fake.checkTokenMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeAuthWithSSBService) CheckTokenCallCount() int { + fake.checkTokenMutex.RLock() + defer fake.checkTokenMutex.RUnlock() + return len(fake.checkTokenArgsForCall) +} + +func (fake *FakeAuthWithSSBService) CheckTokenCalls(stub func(context.Context, string) (int64, error)) { + fake.checkTokenMutex.Lock() + defer fake.checkTokenMutex.Unlock() + fake.CheckTokenStub = stub +} + +func (fake *FakeAuthWithSSBService) CheckTokenArgsForCall(i int) (context.Context, string) { + fake.checkTokenMutex.RLock() + defer fake.checkTokenMutex.RUnlock() + argsForCall := fake.checkTokenArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeAuthWithSSBService) CheckTokenReturns(result1 int64, result2 error) { + fake.checkTokenMutex.Lock() + defer fake.checkTokenMutex.Unlock() + fake.CheckTokenStub = nil + fake.checkTokenReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *FakeAuthWithSSBService) CheckTokenReturnsOnCall(i int, result1 int64, result2 error) { + fake.checkTokenMutex.Lock() + defer fake.checkTokenMutex.Unlock() + fake.CheckTokenStub = nil + if fake.checkTokenReturnsOnCall == nil { + fake.checkTokenReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.checkTokenReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *FakeAuthWithSSBService) CreateToken(arg1 context.Context, arg2 int64) (string, error) { + fake.createTokenMutex.Lock() + ret, specificReturn := fake.createTokenReturnsOnCall[len(fake.createTokenArgsForCall)] + fake.createTokenArgsForCall = append(fake.createTokenArgsForCall, struct { + arg1 context.Context + arg2 int64 + }{arg1, arg2}) + stub := fake.CreateTokenStub + fakeReturns := fake.createTokenReturns + fake.recordInvocation("CreateToken", []interface{}{arg1, arg2}) + fake.createTokenMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeAuthWithSSBService) CreateTokenCallCount() int { + fake.createTokenMutex.RLock() + defer fake.createTokenMutex.RUnlock() + return len(fake.createTokenArgsForCall) +} + +func (fake *FakeAuthWithSSBService) CreateTokenCalls(stub func(context.Context, int64) (string, error)) { + fake.createTokenMutex.Lock() + defer fake.createTokenMutex.Unlock() + fake.CreateTokenStub = stub +} + +func (fake *FakeAuthWithSSBService) CreateTokenArgsForCall(i int) (context.Context, int64) { + fake.createTokenMutex.RLock() + defer fake.createTokenMutex.RUnlock() + argsForCall := fake.createTokenArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeAuthWithSSBService) CreateTokenReturns(result1 string, result2 error) { + fake.createTokenMutex.Lock() + defer fake.createTokenMutex.Unlock() + fake.CreateTokenStub = nil + fake.createTokenReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeAuthWithSSBService) CreateTokenReturnsOnCall(i int, result1 string, result2 error) { + fake.createTokenMutex.Lock() + defer fake.createTokenMutex.Unlock() + fake.CreateTokenStub = nil + if fake.createTokenReturnsOnCall == nil { + fake.createTokenReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createTokenReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeAuthWithSSBService) WipeTokensForMember(arg1 context.Context, arg2 int64) error { + fake.wipeTokensForMemberMutex.Lock() + ret, specificReturn := fake.wipeTokensForMemberReturnsOnCall[len(fake.wipeTokensForMemberArgsForCall)] + fake.wipeTokensForMemberArgsForCall = append(fake.wipeTokensForMemberArgsForCall, struct { + arg1 context.Context + arg2 int64 + }{arg1, arg2}) + stub := fake.WipeTokensForMemberStub + fakeReturns := fake.wipeTokensForMemberReturns + fake.recordInvocation("WipeTokensForMember", []interface{}{arg1, arg2}) + fake.wipeTokensForMemberMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeAuthWithSSBService) WipeTokensForMemberCallCount() int { + fake.wipeTokensForMemberMutex.RLock() + defer fake.wipeTokensForMemberMutex.RUnlock() + return len(fake.wipeTokensForMemberArgsForCall) +} + +func (fake *FakeAuthWithSSBService) WipeTokensForMemberCalls(stub func(context.Context, int64) error) { + fake.wipeTokensForMemberMutex.Lock() + defer fake.wipeTokensForMemberMutex.Unlock() + fake.WipeTokensForMemberStub = stub +} + +func (fake *FakeAuthWithSSBService) WipeTokensForMemberArgsForCall(i int) (context.Context, int64) { + fake.wipeTokensForMemberMutex.RLock() + defer fake.wipeTokensForMemberMutex.RUnlock() + argsForCall := fake.wipeTokensForMemberArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeAuthWithSSBService) WipeTokensForMemberReturns(result1 error) { + fake.wipeTokensForMemberMutex.Lock() + defer fake.wipeTokensForMemberMutex.Unlock() + fake.WipeTokensForMemberStub = nil + fake.wipeTokensForMemberReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeAuthWithSSBService) WipeTokensForMemberReturnsOnCall(i int, result1 error) { + fake.wipeTokensForMemberMutex.Lock() + defer fake.wipeTokensForMemberMutex.Unlock() + fake.WipeTokensForMemberStub = nil + if fake.wipeTokensForMemberReturnsOnCall == nil { + fake.wipeTokensForMemberReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.wipeTokensForMemberReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeAuthWithSSBService) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.checkTokenMutex.RLock() + defer fake.checkTokenMutex.RUnlock() + fake.createTokenMutex.RLock() + defer fake.createTokenMutex.RUnlock() + fake.wipeTokensForMemberMutex.RLock() + defer fake.wipeTokensForMemberMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/roomdb/mockdb/members.go b/roomdb/mockdb/members.go index 5f09ed4..da42346 100644 --- a/roomdb/mockdb/members.go +++ b/roomdb/mockdb/members.go @@ -26,19 +26,6 @@ type FakeMembersService struct { result1 int64 result2 error } - ChangeRoleStub func(context.Context, int64, roomdb.Role) error - changeRoleMutex sync.RWMutex - changeRoleArgsForCall []struct { - arg1 context.Context - arg2 int64 - arg3 roomdb.Role - } - changeRoleReturns struct { - result1 error - } - changeRoleReturnsOnCall map[int]struct { - result1 error - } GetByFeedStub func(context.Context, refs.FeedRef) (roomdb.Member, error) getByFeedMutex sync.RWMutex getByFeedArgsForCall []struct { @@ -188,69 +175,6 @@ func (fake *FakeMembersService) AddReturnsOnCall(i int, result1 int64, result2 e }{result1, result2} } -func (fake *FakeMembersService) ChangeRole(arg1 context.Context, arg2 int64, arg3 roomdb.Role) error { - fake.changeRoleMutex.Lock() - ret, specificReturn := fake.changeRoleReturnsOnCall[len(fake.changeRoleArgsForCall)] - fake.changeRoleArgsForCall = append(fake.changeRoleArgsForCall, struct { - arg1 context.Context - arg2 int64 - arg3 roomdb.Role - }{arg1, arg2, arg3}) - stub := fake.ChangeRoleStub - fakeReturns := fake.changeRoleReturns - fake.recordInvocation("ChangeRole", []interface{}{arg1, arg2, arg3}) - fake.changeRoleMutex.Unlock() - if stub != nil { - return stub(arg1, arg2, arg3) - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeMembersService) ChangeRoleCallCount() int { - fake.changeRoleMutex.RLock() - defer fake.changeRoleMutex.RUnlock() - return len(fake.changeRoleArgsForCall) -} - -func (fake *FakeMembersService) ChangeRoleCalls(stub func(context.Context, int64, roomdb.Role) error) { - fake.changeRoleMutex.Lock() - defer fake.changeRoleMutex.Unlock() - fake.ChangeRoleStub = stub -} - -func (fake *FakeMembersService) ChangeRoleArgsForCall(i int) (context.Context, int64, roomdb.Role) { - fake.changeRoleMutex.RLock() - defer fake.changeRoleMutex.RUnlock() - argsForCall := fake.changeRoleArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 -} - -func (fake *FakeMembersService) ChangeRoleReturns(result1 error) { - fake.changeRoleMutex.Lock() - defer fake.changeRoleMutex.Unlock() - fake.ChangeRoleStub = nil - fake.changeRoleReturns = struct { - result1 error - }{result1} -} - -func (fake *FakeMembersService) ChangeRoleReturnsOnCall(i int, result1 error) { - fake.changeRoleMutex.Lock() - defer fake.changeRoleMutex.Unlock() - fake.ChangeRoleStub = nil - if fake.changeRoleReturnsOnCall == nil { - fake.changeRoleReturnsOnCall = make(map[int]struct { - result1 error - }) - } - fake.changeRoleReturnsOnCall[i] = struct { - result1 error - }{result1} -} - func (fake *FakeMembersService) GetByFeed(arg1 context.Context, arg2 refs.FeedRef) (roomdb.Member, error) { fake.getByFeedMutex.Lock() ret, specificReturn := fake.getByFeedReturnsOnCall[len(fake.getByFeedArgsForCall)] @@ -637,8 +561,6 @@ func (fake *FakeMembersService) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.addMutex.RLock() defer fake.addMutex.RUnlock() - fake.changeRoleMutex.RLock() - defer fake.changeRoleMutex.RUnlock() fake.getByFeedMutex.RLock() defer fake.getByFeedMutex.RUnlock() fake.getByIDMutex.RLock() diff --git a/roomdb/sqlite/auth_withssb.go b/roomdb/sqlite/auth_withssb.go index 5a8902c..ccd51a7 100644 --- a/roomdb/sqlite/auth_withssb.go +++ b/roomdb/sqlite/auth_withssb.go @@ -3,9 +3,18 @@ package sqlite import ( + "context" "database/sql" + "time" + "github.com/friendsofgo/errors" + "github.com/mattn/go-sqlite3" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + + "github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite/models" ) // compiler assertion to ensure the struct fullfills the interface @@ -14,3 +23,107 @@ var _ roomdb.AuthWithSSBService = (*AuthWithSSB)(nil) type AuthWithSSB struct { db *sql.DB } + +const siwssbTokenLength = 32 + +// CreateToken is used to generate a token that is stored inside a cookie. +// It is used after a valid solution for a challenge was provided. +func (a AuthWithSSB) CreateToken(ctx context.Context, memberID int64) (string, error) { + + var newToken = models.SIWSSBSession{ + MemberID: memberID, + } + + err := transact(a.db, func(tx *sql.Tx) error { + + // check the member is registerd + if _, err := models.FindMember(ctx, tx, newToken.MemberID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return roomdb.ErrNotFound + } + return err + } + + inserted := false + trying: // keep trying until we inserted in unused token + for tries := 100; tries > 0; tries-- { + + // generate an new token + newToken.Token = randutil.String(siwssbTokenLength) + + // insert the new token + cols := boil.Whitelist(models.SIWSSBSessionColumns.Token, models.SIWSSBSessionColumns.MemberID) + err := newToken.Insert(ctx, tx, cols) + if err != nil { + var sqlErr sqlite3.Error + if errors.As(err, &sqlErr) && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique { + // generated an existing token, retry + continue trying + } + return err + } + inserted = true + break // no error means it worked! + } + + if !inserted { + return errors.New("admindb: failed to generate a fresh token in a reasonable amount of time") + } + + return nil + }) + + if err != nil { + return "", err + } + + return newToken.Token, nil +} + +const sessionTimeout = time.Hour * 24 + +// CheckToken checks if the passed token is still valid and returns the member id if so +func (a AuthWithSSB) CheckToken(ctx context.Context, token string) (int64, error) { + var memberID int64 + + err := transact(a.db, func(tx *sql.Tx) error { + session, err := models.SIWSSBSessions(qm.Where("token = ?", token)).One(ctx, a.db) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return roomdb.ErrNotFound + } + return err + } + + if time.Since(session.CreatedAt) > sessionTimeout { + _, err = session.Delete(ctx, tx) + if err != nil { + return err + } + + return errors.New("sign-in with ssb: session expired") + } + + memberID = session.MemberID + return nil + }) + if err != nil { + return -1, err + } + + return memberID, nil +} + +// WipeTokensForMember deletes all tokens currently held for that member +func (a AuthWithSSB) WipeTokensForMember(ctx context.Context, memberID int64) error { + return transact(a.db, func(tx *sql.Tx) error { + _, err := models.SIWSSBSessions(qm.Where("member_id = ?", memberID)).DeleteAll(ctx, tx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return roomdb.ErrNotFound + } + return err + } + return nil + }) +} diff --git a/roomdb/sqlite/invites.go b/roomdb/sqlite/invites.go index 0fc7909..261623a 100644 --- a/roomdb/sqlite/invites.go +++ b/roomdb/sqlite/invites.go @@ -33,14 +33,14 @@ type Invites struct { // Create creates a new invite for a new member. It returns the token or an error. // createdBy is user ID of the admin or moderator who created it. // aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21) -// The returned token is base64 URL encoded and has tokenLength when decoded. +// The returned token is base64 URL encoded and has inviteTokenLength when decoded. func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error) { var newInvite = models.Invite{ CreatedBy: createdBy, AliasSuggestion: aliasSuggestion, } - tokenBytes := make([]byte, tokenLength) + tokenBytes := make([]byte, inviteTokenLength) err := transact(i.db, func(tx *sql.Tx) error { @@ -88,7 +88,7 @@ func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion st // Consume checks if the passed token is still valid. If it is it adds newMember to the members of the room and invalidates the token. // If the token isn't valid, it returns an error. -// Tokens need to be base64 URL encoded and when decoded be of tokenLength. +// Tokens need to be base64 URL encoded and when decoded be of inviteTokenLength. func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedRef) (roomdb.Invite, error) { var inv roomdb.Invite @@ -263,7 +263,7 @@ func (i Invites) Revoke(ctx context.Context, id int64) error { }) } -const tokenLength = 50 +const inviteTokenLength = 50 func getHashedToken(b64tok string) (string, error) { tokenBytes, err := base64.URLEncoding.DecodeString(b64tok) @@ -271,7 +271,7 @@ func getHashedToken(b64tok string) (string, error) { return "", err } - if n := len(tokenBytes); n != tokenLength { + if n := len(tokenBytes); n != inviteTokenLength { return "", fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n) } diff --git a/roomdb/sqlite/migrations/03-siwssb-tokens.sql b/roomdb/sqlite/migrations/03-siwssb-tokens.sql new file mode 100644 index 0000000..5163c4f --- /dev/null +++ b/roomdb/sqlite/migrations/03-siwssb-tokens.sql @@ -0,0 +1,17 @@ +-- +migrate Up +-- SIWSSB stands for sign-in with ssb +CREATE TABLE SIWSSB_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + token TEXT UNIQUE NOT NULL, + member_id INTEGER NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY ( member_id ) REFERENCES members( "id" ) +); +CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token); +CREATE UNIQUE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id); + +-- +migrate Down +DROP TABLE SIWSSB_sessions; +DROP INDEX SIWSSB_by_token; +DROP INDEX SIWSSB_by_member; \ No newline at end of file diff --git a/roomdb/sqlite/models/SIWSSB_sessions.go b/roomdb/sqlite/models/SIWSSB_sessions.go new file mode 100644 index 0000000..e6c8cc4 --- /dev/null +++ b/roomdb/sqlite/models/SIWSSB_sessions.go @@ -0,0 +1,1032 @@ +// Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strings" + "sync" + "time" + + "github.com/friendsofgo/errors" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + "github.com/volatiletech/sqlboiler/v4/queries/qmhelper" + "github.com/volatiletech/strmangle" +) + +// SIWSSBSession is an object representing the database table. +type SIWSSBSession struct { + ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"` + Token string `boil:"token" json:"token" toml:"token" yaml:"token"` + MemberID int64 `boil:"member_id" json:"member_id" toml:"member_id" yaml:"member_id"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + + R *sIWSSBSessionR `boil:"-" json:"-" toml:"-" yaml:"-"` + L sIWSSBSessionL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var SIWSSBSessionColumns = struct { + ID string + Token string + MemberID string + CreatedAt string +}{ + ID: "id", + Token: "token", + MemberID: "member_id", + CreatedAt: "created_at", +} + +// Generated where + +type whereHelperint64 struct{ field string } + +func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperint64) IN(slice []int64) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelperint64) NIN(slice []int64) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +type whereHelperstring struct{ field string } + +func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } +func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } +func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } +func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } +func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } +func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +func (w whereHelperstring) IN(slice []string) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelperstring) NIN(slice []string) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +type whereHelpertime_Time struct{ field string } + +func (w whereHelpertime_Time) EQ(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.EQ, x) +} +func (w whereHelpertime_Time) NEQ(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.NEQ, x) +} +func (w whereHelpertime_Time) LT(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpertime_Time) LTE(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpertime_Time) GT(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} + +var SIWSSBSessionWhere = struct { + ID whereHelperint64 + Token whereHelperstring + MemberID whereHelperint64 + CreatedAt whereHelpertime_Time +}{ + ID: whereHelperint64{field: "\"SIWSSB_sessions\".\"id\""}, + Token: whereHelperstring{field: "\"SIWSSB_sessions\".\"token\""}, + MemberID: whereHelperint64{field: "\"SIWSSB_sessions\".\"member_id\""}, + CreatedAt: whereHelpertime_Time{field: "\"SIWSSB_sessions\".\"created_at\""}, +} + +// SIWSSBSessionRels is where relationship names are stored. +var SIWSSBSessionRels = struct { + Member string +}{ + Member: "Member", +} + +// sIWSSBSessionR is where relationships are stored. +type sIWSSBSessionR struct { + Member *Member `boil:"Member" json:"Member" toml:"Member" yaml:"Member"` +} + +// NewStruct creates a new relationship struct +func (*sIWSSBSessionR) NewStruct() *sIWSSBSessionR { + return &sIWSSBSessionR{} +} + +// sIWSSBSessionL is where Load methods for each relationship are stored. +type sIWSSBSessionL struct{} + +var ( + sIWSSBSessionAllColumns = []string{"id", "token", "member_id", "created_at"} + sIWSSBSessionColumnsWithoutDefault = []string{} + sIWSSBSessionColumnsWithDefault = []string{"id", "token", "member_id", "created_at"} + sIWSSBSessionPrimaryKeyColumns = []string{"id"} +) + +type ( + // SIWSSBSessionSlice is an alias for a slice of pointers to SIWSSBSession. + // This should generally be used opposed to []SIWSSBSession. + SIWSSBSessionSlice []*SIWSSBSession + // SIWSSBSessionHook is the signature for custom SIWSSBSession hook methods + SIWSSBSessionHook func(context.Context, boil.ContextExecutor, *SIWSSBSession) error + + sIWSSBSessionQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + sIWSSBSessionType = reflect.TypeOf(&SIWSSBSession{}) + sIWSSBSessionMapping = queries.MakeStructMapping(sIWSSBSessionType) + sIWSSBSessionPrimaryKeyMapping, _ = queries.BindMapping(sIWSSBSessionType, sIWSSBSessionMapping, sIWSSBSessionPrimaryKeyColumns) + sIWSSBSessionInsertCacheMut sync.RWMutex + sIWSSBSessionInsertCache = make(map[string]insertCache) + sIWSSBSessionUpdateCacheMut sync.RWMutex + sIWSSBSessionUpdateCache = make(map[string]updateCache) + sIWSSBSessionUpsertCacheMut sync.RWMutex + sIWSSBSessionUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +var sIWSSBSessionBeforeInsertHooks []SIWSSBSessionHook +var sIWSSBSessionBeforeUpdateHooks []SIWSSBSessionHook +var sIWSSBSessionBeforeDeleteHooks []SIWSSBSessionHook +var sIWSSBSessionBeforeUpsertHooks []SIWSSBSessionHook + +var sIWSSBSessionAfterInsertHooks []SIWSSBSessionHook +var sIWSSBSessionAfterSelectHooks []SIWSSBSessionHook +var sIWSSBSessionAfterUpdateHooks []SIWSSBSessionHook +var sIWSSBSessionAfterDeleteHooks []SIWSSBSessionHook +var sIWSSBSessionAfterUpsertHooks []SIWSSBSessionHook + +// doBeforeInsertHooks executes all "before insert" hooks. +func (o *SIWSSBSession) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionBeforeInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpdateHooks executes all "before Update" hooks. +func (o *SIWSSBSession) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionBeforeUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeDeleteHooks executes all "before Delete" hooks. +func (o *SIWSSBSession) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionBeforeDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpsertHooks executes all "before Upsert" hooks. +func (o *SIWSSBSession) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionBeforeUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterInsertHooks executes all "after Insert" hooks. +func (o *SIWSSBSession) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionAfterInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterSelectHooks executes all "after Select" hooks. +func (o *SIWSSBSession) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionAfterSelectHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpdateHooks executes all "after Update" hooks. +func (o *SIWSSBSession) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionAfterUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterDeleteHooks executes all "after Delete" hooks. +func (o *SIWSSBSession) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionAfterDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpsertHooks executes all "after Upsert" hooks. +func (o *SIWSSBSession) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range sIWSSBSessionAfterUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// AddSIWSSBSessionHook registers your hook function for all future operations. +func AddSIWSSBSessionHook(hookPoint boil.HookPoint, sIWSSBSessionHook SIWSSBSessionHook) { + switch hookPoint { + case boil.BeforeInsertHook: + sIWSSBSessionBeforeInsertHooks = append(sIWSSBSessionBeforeInsertHooks, sIWSSBSessionHook) + case boil.BeforeUpdateHook: + sIWSSBSessionBeforeUpdateHooks = append(sIWSSBSessionBeforeUpdateHooks, sIWSSBSessionHook) + case boil.BeforeDeleteHook: + sIWSSBSessionBeforeDeleteHooks = append(sIWSSBSessionBeforeDeleteHooks, sIWSSBSessionHook) + case boil.BeforeUpsertHook: + sIWSSBSessionBeforeUpsertHooks = append(sIWSSBSessionBeforeUpsertHooks, sIWSSBSessionHook) + case boil.AfterInsertHook: + sIWSSBSessionAfterInsertHooks = append(sIWSSBSessionAfterInsertHooks, sIWSSBSessionHook) + case boil.AfterSelectHook: + sIWSSBSessionAfterSelectHooks = append(sIWSSBSessionAfterSelectHooks, sIWSSBSessionHook) + case boil.AfterUpdateHook: + sIWSSBSessionAfterUpdateHooks = append(sIWSSBSessionAfterUpdateHooks, sIWSSBSessionHook) + case boil.AfterDeleteHook: + sIWSSBSessionAfterDeleteHooks = append(sIWSSBSessionAfterDeleteHooks, sIWSSBSessionHook) + case boil.AfterUpsertHook: + sIWSSBSessionAfterUpsertHooks = append(sIWSSBSessionAfterUpsertHooks, sIWSSBSessionHook) + } +} + +// One returns a single sIWSSBSession record from the query. +func (q sIWSSBSessionQuery) One(ctx context.Context, exec boil.ContextExecutor) (*SIWSSBSession, error) { + o := &SIWSSBSession{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: failed to execute a one query for SIWSSB_sessions") + } + + if err := o.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + + return o, nil +} + +// All returns all SIWSSBSession records from the query. +func (q sIWSSBSessionQuery) All(ctx context.Context, exec boil.ContextExecutor) (SIWSSBSessionSlice, error) { + var o []*SIWSSBSession + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "models: failed to assign all query results to SIWSSBSession slice") + } + + if len(sIWSSBSessionAfterSelectHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + } + } + + return o, nil +} + +// Count returns the count of all SIWSSBSession records in the query. +func (q sIWSSBSessionQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "models: failed to count SIWSSB_sessions rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q sIWSSBSessionQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "models: failed to check if SIWSSB_sessions exists") + } + + return count > 0, nil +} + +// Member pointed to by the foreign key. +func (o *SIWSSBSession) Member(mods ...qm.QueryMod) memberQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"id\" = ?", o.MemberID), + } + + queryMods = append(queryMods, mods...) + + query := Members(queryMods...) + queries.SetFrom(query.Query, "\"members\"") + + return query +} + +// LoadMember allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for an N-1 relationship. +func (sIWSSBSessionL) LoadMember(ctx context.Context, e boil.ContextExecutor, singular bool, maybeSIWSSBSession interface{}, mods queries.Applicator) error { + var slice []*SIWSSBSession + var object *SIWSSBSession + + if singular { + object = maybeSIWSSBSession.(*SIWSSBSession) + } else { + slice = *maybeSIWSSBSession.(*[]*SIWSSBSession) + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &sIWSSBSessionR{} + } + args = append(args, object.MemberID) + + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &sIWSSBSessionR{} + } + + for _, a := range args { + if a == obj.MemberID { + continue Outer + } + } + + args = append(args, obj.MemberID) + + } + } + + if len(args) == 0 { + return nil + } + + query := NewQuery( + qm.From(`members`), + qm.WhereIn(`members.id in ?`, args...), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load Member") + } + + var resultSlice []*Member + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice Member") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for members") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for members") + } + + if len(sIWSSBSessionAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + + if len(resultSlice) == 0 { + return nil + } + + if singular { + foreign := resultSlice[0] + object.R.Member = foreign + if foreign.R == nil { + foreign.R = &memberR{} + } + foreign.R.SIWSSBSessions = append(foreign.R.SIWSSBSessions, object) + return nil + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if local.MemberID == foreign.ID { + local.R.Member = foreign + if foreign.R == nil { + foreign.R = &memberR{} + } + foreign.R.SIWSSBSessions = append(foreign.R.SIWSSBSessions, local) + break + } + } + } + + return nil +} + +// SetMember of the sIWSSBSession to the related item. +// Sets o.R.Member to related. +// Adds o to related.R.SIWSSBSessions. +func (o *SIWSSBSession) SetMember(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Member) error { + var err error + if insert { + if err = related.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } + + updateQuery := fmt.Sprintf( + "UPDATE \"SIWSSB_sessions\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}), + strmangle.WhereClause("\"", "\"", 0, sIWSSBSessionPrimaryKeyColumns), + ) + values := []interface{}{related.ID, o.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + o.MemberID = related.ID + if o.R == nil { + o.R = &sIWSSBSessionR{ + Member: related, + } + } else { + o.R.Member = related + } + + if related.R == nil { + related.R = &memberR{ + SIWSSBSessions: SIWSSBSessionSlice{o}, + } + } else { + related.R.SIWSSBSessions = append(related.R.SIWSSBSessions, o) + } + + return nil +} + +// SIWSSBSessions retrieves all the records using an executor. +func SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery { + mods = append(mods, qm.From("\"SIWSSB_sessions\"")) + return sIWSSBSessionQuery{NewQuery(mods...)} +} + +// FindSIWSSBSession retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindSIWSSBSession(ctx context.Context, exec boil.ContextExecutor, iD int64, selectCols ...string) (*SIWSSBSession, error) { + sIWSSBSessionObj := &SIWSSBSession{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"SIWSSB_sessions\" where \"id\"=?", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, sIWSSBSessionObj) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: unable to select from SIWSSB_sessions") + } + + return sIWSSBSessionObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *SIWSSBSession) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("models: no SIWSSB_sessions provided for insertion") + } + + var err error + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + } + + if err := o.doBeforeInsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(sIWSSBSessionColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + sIWSSBSessionInsertCacheMut.RLock() + cache, cached := sIWSSBSessionInsertCache[key] + sIWSSBSessionInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + sIWSSBSessionAllColumns, + sIWSSBSessionColumnsWithDefault, + sIWSSBSessionColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(sIWSSBSessionType, sIWSSBSessionMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(sIWSSBSessionType, sIWSSBSessionMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"SIWSSB_sessions\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"SIWSSB_sessions\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + cache.retQuery = fmt.Sprintf("SELECT \"%s\" FROM \"SIWSSB_sessions\" WHERE %s", strings.Join(returnColumns, "\",\""), strmangle.WhereClause("\"", "\"", 0, sIWSSBSessionPrimaryKeyColumns)) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + result, err := exec.ExecContext(ctx, cache.query, vals...) + + if err != nil { + return errors.Wrap(err, "models: unable to insert into SIWSSB_sessions") + } + + var lastID int64 + var identifierCols []interface{} + + if len(cache.retMapping) == 0 { + goto CacheNoHooks + } + + lastID, err = result.LastInsertId() + if err != nil { + return ErrSyncFail + } + + o.ID = int64(lastID) + if lastID != 0 && len(cache.retMapping) == 1 && cache.retMapping[0] == sIWSSBSessionMapping["id"] { + goto CacheNoHooks + } + + identifierCols = []interface{}{ + o.ID, + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.retQuery) + fmt.Fprintln(writer, identifierCols...) + } + err = exec.QueryRowContext(ctx, cache.retQuery, identifierCols...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + if err != nil { + return errors.Wrap(err, "models: unable to populate default values for SIWSSB_sessions") + } + +CacheNoHooks: + if !cached { + sIWSSBSessionInsertCacheMut.Lock() + sIWSSBSessionInsertCache[key] = cache + sIWSSBSessionInsertCacheMut.Unlock() + } + + return o.doAfterInsertHooks(ctx, exec) +} + +// Update uses an executor to update the SIWSSBSession. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *SIWSSBSession) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + var err error + if err = o.doBeforeUpdateHooks(ctx, exec); err != nil { + return 0, err + } + key := makeCacheKey(columns, nil) + sIWSSBSessionUpdateCacheMut.RLock() + cache, cached := sIWSSBSessionUpdateCache[key] + sIWSSBSessionUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + sIWSSBSessionAllColumns, + sIWSSBSessionPrimaryKeyColumns, + ) + + if !columns.IsWhitelist() { + wl = strmangle.SetComplement(wl, []string{"created_at"}) + } + if len(wl) == 0 { + return 0, errors.New("models: unable to update SIWSSB_sessions, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"SIWSSB_sessions\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, wl), + strmangle.WhereClause("\"", "\"", 0, sIWSSBSessionPrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(sIWSSBSessionType, sIWSSBSessionMapping, append(wl, sIWSSBSessionPrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, values) + } + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update SIWSSB_sessions row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by update for SIWSSB_sessions") + } + + if !cached { + sIWSSBSessionUpdateCacheMut.Lock() + sIWSSBSessionUpdateCache[key] = cache + sIWSSBSessionUpdateCacheMut.Unlock() + } + + return rowsAff, o.doAfterUpdateHooks(ctx, exec) +} + +// UpdateAll updates all rows with the specified column values. +func (q sIWSSBSessionQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update all for SIWSSB_sessions") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected for SIWSSB_sessions") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o SIWSSBSessionSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("models: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), sIWSSBSessionPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"SIWSSB_sessions\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, sIWSSBSessionPrimaryKeyColumns, len(o))) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update all in sIWSSBSession slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all sIWSSBSession") + } + return rowsAff, nil +} + +// Delete deletes a single SIWSSBSession record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *SIWSSBSession) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("models: no SIWSSBSession provided for delete") + } + + if err := o.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), sIWSSBSessionPrimaryKeyMapping) + sql := "DELETE FROM \"SIWSSB_sessions\" WHERE \"id\"=?" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete from SIWSSB_sessions") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by delete for SIWSSB_sessions") + } + + if err := o.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q sIWSSBSessionQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("models: no sIWSSBSessionQuery provided for delete all") + } + + queries.SetDelete(q.Query) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete all from SIWSSB_sessions") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for SIWSSB_sessions") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o SIWSSBSessionSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + if len(sIWSSBSessionBeforeDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + var args []interface{} + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), sIWSSBSessionPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"SIWSSB_sessions\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, sIWSSBSessionPrimaryKeyColumns, len(o)) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete all from sIWSSBSession slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for SIWSSB_sessions") + } + + if len(sIWSSBSessionAfterDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + return rowsAff, nil +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *SIWSSBSession) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindSIWSSBSession(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *SIWSSBSessionSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := SIWSSBSessionSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), sIWSSBSessionPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"SIWSSB_sessions\".* FROM \"SIWSSB_sessions\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 0, sIWSSBSessionPrimaryKeyColumns, len(*o)) + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "models: unable to reload all in SIWSSBSessionSlice") + } + + *o = slice + + return nil +} + +// SIWSSBSessionExists checks if the SIWSSBSession row exists. +func SIWSSBSessionExists(ctx context.Context, exec boil.ContextExecutor, iD int64) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"SIWSSB_sessions\" where \"id\"=? limit 1)" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, iD) + } + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "models: unable to check if SIWSSB_sessions exists") + } + + return exists, nil +} diff --git a/roomdb/sqlite/models/aliases.go b/roomdb/sqlite/models/aliases.go index abb9a4a..c38a9ec 100644 --- a/roomdb/sqlite/models/aliases.go +++ b/roomdb/sqlite/models/aliases.go @@ -45,52 +45,6 @@ var AliasColumns = struct { // Generated where -type whereHelperint64 struct{ field string } - -func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } -func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } -func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } -func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } -func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } -func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } -func (w whereHelperint64) IN(slice []int64) qm.QueryMod { - values := make([]interface{}, 0, len(slice)) - for _, value := range slice { - values = append(values, value) - } - return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) -} -func (w whereHelperint64) NIN(slice []int64) qm.QueryMod { - values := make([]interface{}, 0, len(slice)) - for _, value := range slice { - values = append(values, value) - } - return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) -} - -type whereHelperstring struct{ field string } - -func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } -func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) } -func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) } -func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) } -func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) } -func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } -func (w whereHelperstring) IN(slice []string) qm.QueryMod { - values := make([]interface{}, 0, len(slice)) - for _, value := range slice { - values = append(values, value) - } - return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) -} -func (w whereHelperstring) NIN(slice []string) qm.QueryMod { - values := make([]interface{}, 0, len(slice)) - for _, value := range slice { - values = append(values, value) - } - return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) -} - type whereHelper__byte struct{ field string } func (w whereHelper__byte) EQ(x []byte) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) } diff --git a/roomdb/sqlite/models/boil_table_names.go b/roomdb/sqlite/models/boil_table_names.go index 162de03..ea8aa6a 100644 --- a/roomdb/sqlite/models/boil_table_names.go +++ b/roomdb/sqlite/models/boil_table_names.go @@ -4,6 +4,7 @@ package models var TableNames = struct { + SIWSSBSessions string Aliases string DeniedKeys string FallbackPasswords string @@ -13,6 +14,7 @@ var TableNames = struct { PinNotices string Pins string }{ + SIWSSBSessions: "SIWSSB_sessions", Aliases: "aliases", DeniedKeys: "denied_keys", FallbackPasswords: "fallback_passwords", diff --git a/roomdb/sqlite/models/denied_keys.go b/roomdb/sqlite/models/denied_keys.go index 95286ec..457d6fd 100644 --- a/roomdb/sqlite/models/denied_keys.go +++ b/roomdb/sqlite/models/denied_keys.go @@ -67,27 +67,6 @@ func (w whereHelperroomdb_DBFeedRef) GTE(x roomdb.DBFeedRef) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } -type whereHelpertime_Time struct{ field string } - -func (w whereHelpertime_Time) EQ(x time.Time) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.EQ, x) -} -func (w whereHelpertime_Time) NEQ(x time.Time) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.NEQ, x) -} -func (w whereHelpertime_Time) LT(x time.Time) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.LT, x) -} -func (w whereHelpertime_Time) LTE(x time.Time) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.LTE, x) -} -func (w whereHelpertime_Time) GT(x time.Time) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.GT, x) -} -func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.GTE, x) -} - var DeniedKeyWhere = struct { ID whereHelperint64 PubKey whereHelperroomdb_DBFeedRef diff --git a/roomdb/sqlite/models/members.go b/roomdb/sqlite/models/members.go index 21292e6..2d6f529 100644 --- a/roomdb/sqlite/models/members.go +++ b/roomdb/sqlite/models/members.go @@ -60,10 +60,12 @@ var MemberWhere = struct { // MemberRels is where relationship names are stored. var MemberRels = struct { + SIWSSBSessions string Aliases string FallbackPasswords string CreatedByInvites string }{ + SIWSSBSessions: "SIWSSBSessions", Aliases: "Aliases", FallbackPasswords: "FallbackPasswords", CreatedByInvites: "CreatedByInvites", @@ -71,6 +73,7 @@ var MemberRels = struct { // memberR is where relationships are stored. type memberR struct { + SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"` Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"` FallbackPasswords FallbackPasswordSlice `boil:"FallbackPasswords" json:"FallbackPasswords" toml:"FallbackPasswords" yaml:"FallbackPasswords"` CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"` @@ -366,6 +369,27 @@ func (q memberQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (boo return count > 0, nil } +// SIWSSBSessions retrieves all the SIWSSB_session's SIWSSBSessions with an executor. +func (o *Member) SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery { + var queryMods []qm.QueryMod + if len(mods) != 0 { + queryMods = append(queryMods, mods...) + } + + queryMods = append(queryMods, + qm.Where("\"SIWSSB_sessions\".\"member_id\"=?", o.ID), + ) + + query := SIWSSBSessions(queryMods...) + queries.SetFrom(query.Query, "\"SIWSSB_sessions\"") + + if len(queries.GetSelect(query.Query)) == 0 { + queries.SetSelect(query.Query, []string{"\"SIWSSB_sessions\".*"}) + } + + return query +} + // Aliases retrieves all the alias's Aliases with an executor. func (o *Member) Aliases(mods ...qm.QueryMod) aliasQuery { var queryMods []qm.QueryMod @@ -429,6 +453,104 @@ func (o *Member) CreatedByInvites(mods ...qm.QueryMod) inviteQuery { return query } +// LoadSIWSSBSessions allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for a 1-M or N-M relationship. +func (memberL) LoadSIWSSBSessions(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { + var slice []*Member + var object *Member + + if singular { + object = maybeMember.(*Member) + } else { + slice = *maybeMember.(*[]*Member) + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &memberR{} + } + args = append(args, object.ID) + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &memberR{} + } + + for _, a := range args { + if a == obj.ID { + continue Outer + } + } + + args = append(args, obj.ID) + } + } + + if len(args) == 0 { + return nil + } + + query := NewQuery( + qm.From(`SIWSSB_sessions`), + qm.WhereIn(`SIWSSB_sessions.member_id in ?`, args...), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load SIWSSB_sessions") + } + + var resultSlice []*SIWSSBSession + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice SIWSSB_sessions") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results in eager load on SIWSSB_sessions") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for SIWSSB_sessions") + } + + if len(sIWSSBSessionAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + if singular { + object.R.SIWSSBSessions = resultSlice + for _, foreign := range resultSlice { + if foreign.R == nil { + foreign.R = &sIWSSBSessionR{} + } + foreign.R.Member = object + } + return nil + } + + for _, foreign := range resultSlice { + for _, local := range slice { + if local.ID == foreign.MemberID { + local.R.SIWSSBSessions = append(local.R.SIWSSBSessions, foreign) + if foreign.R == nil { + foreign.R = &sIWSSBSessionR{} + } + foreign.R.Member = local + break + } + } + } + + return nil +} + // LoadAliases allows an eager lookup of values, cached into the // loaded structs of the objects. This is for a 1-M or N-M relationship. func (memberL) LoadAliases(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error { @@ -723,6 +845,59 @@ func (memberL) LoadCreatedByInvites(ctx context.Context, e boil.ContextExecutor, return nil } +// AddSIWSSBSessions adds the given related objects to the existing relationships +// of the member, optionally inserting them as new records. +// Appends related to o.R.SIWSSBSessions. +// Sets related.R.Member appropriately. +func (o *Member) AddSIWSSBSessions(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*SIWSSBSession) error { + var err error + for _, rel := range related { + if insert { + rel.MemberID = o.ID + if err = rel.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } else { + updateQuery := fmt.Sprintf( + "UPDATE \"SIWSSB_sessions\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}), + strmangle.WhereClause("\"", "\"", 0, sIWSSBSessionPrimaryKeyColumns), + ) + values := []interface{}{o.ID, rel.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update foreign table") + } + + rel.MemberID = o.ID + } + } + + if o.R == nil { + o.R = &memberR{ + SIWSSBSessions: related, + } + } else { + o.R.SIWSSBSessions = append(o.R.SIWSSBSessions, related...) + } + + for _, rel := range related { + if rel.R == nil { + rel.R = &sIWSSBSessionR{ + Member: o, + } + } else { + rel.R.Member = o + } + } + return nil +} + // AddAliases adds the given related objects to the existing relationships // of the member, optionally inserting them as new records. // Appends related to o.R.Aliases. diff --git a/roomdb/sqlite/new.go b/roomdb/sqlite/new.go index 015763e..3bcc819 100644 --- a/roomdb/sqlite/new.go +++ b/roomdb/sqlite/new.go @@ -27,13 +27,13 @@ import ( migrate "github.com/rubenv/sql-migrate" "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" - "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" ) type Database struct { db *sql.DB - AuthFallback roomdb.AuthFallbackService + AuthFallback AuthFallback + AuthWithSSB AuthWithSSB Members Members Aliases Aliases @@ -102,6 +102,7 @@ func Open(r repo.Interface) (*Database, error) { Aliases: Aliases{db}, AuthFallback: AuthFallback{db}, + AuthWithSSB: AuthWithSSB{db}, DeniedKeys: DeniedKeys{db}, Invites: Invites{db: db, members: ml}, Notices: Notices{db}, diff --git a/roomsrv/init_handlers.go b/roomsrv/init_handlers.go index 28c2e96..5e253eb 100644 --- a/roomsrv/init_handlers.go +++ b/roomsrv/init_handlers.go @@ -4,6 +4,7 @@ package roomsrv import ( kitlog "github.com/go-kit/kit/log" + "github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/signinwithssb" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" muxrpc "go.cryptoscope.co/muxrpc/v2" "go.cryptoscope.co/muxrpc/v2/typemux" @@ -31,6 +32,10 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) { s.domain, ) + siwssbHandler := signinwithssb.New( + kitlog.With(s.logger, "unit", "auth-with-ssb"), + ) + // register muxrpc commands registries := []typemux.HandlerMux{s.public, s.master} @@ -47,5 +52,10 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) { mux.RegisterAsync(append(method, "registerAlias"), typemux.AsyncFunc(aliasHandler.Register)) mux.RegisterAsync(append(method, "revokeAlias"), typemux.AsyncFunc(aliasHandler.Revoke)) + + method = muxrpc.Method{"httpAuth"} + mux.RegisterAsync(append(method, "invalidateAllSolutions"), typemux.AsyncFunc(siwssbHandler.InvalidateAllSolutions)) + mux.RegisterAsync(append(method, "sendSolution"), typemux.AsyncFunc(siwssbHandler.SendSolution)) + } } diff --git a/web/errors/badrequest.go b/web/errors/badrequest.go index 44d1900..da82ca9 100644 --- a/web/errors/badrequest.go +++ b/web/errors/badrequest.go @@ -4,6 +4,7 @@ package errors import ( + "errors" "fmt" ) @@ -23,3 +24,13 @@ type ErrBadRequest struct { func (br ErrBadRequest) Error() string { return fmt.Sprintf("rooms/web: bad request error: %s", br.Details) } + +type ErrForbidden struct { + Details error +} + +func (f ErrForbidden) Error() string { + return fmt.Sprintf("rooms/web: access denied: %s", f.Details) +} + +var ErrNotAuthorized = errors.New("rooms/web: not authorized") diff --git a/web/handlers/admin/setup_test.go b/web/handlers/admin/setup_test.go index 2062726..03e21f9 100644 --- a/web/handlers/admin/setup_test.go +++ b/web/handlers/admin/setup_test.go @@ -4,7 +4,6 @@ package admin import ( "context" - "math/rand" "net/http" "testing" "time" @@ -16,6 +15,7 @@ import ( "go.mindeco.de/http/tester" "go.mindeco.de/logging/logtest" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb" "github.com/ssb-ngi-pointer/go-ssb-room/roomstate" @@ -60,7 +60,7 @@ func newSession(t *testing.T) *testSession { ts.Router = router.CompleteApp() - ts.Domain = randomString(10) + ts.Domain = randutil.String(10) // fake user ts.User = roomdb.Member{ @@ -120,15 +120,3 @@ func newSession(t *testing.T) *testSession { return &ts } - -// utils - -func randomString(n int) string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - - s := make([]rune, n) - for i := range s { - s[i] = letters[rand.Intn(len(letters))] - } - return string(s) -} diff --git a/web/handlers/auth/handler.go b/web/handlers/auth/handler.go index de1cf58..fb00086 100644 --- a/web/handlers/auth/handler.go +++ b/web/handlers/auth/handler.go @@ -15,13 +15,19 @@ import ( var HTMLTemplates = []string{ "auth/fallback_sign_in.tmpl", + "auth/withssb_sign_in.tmpl", } -func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler { +func NewFallbackPasswordHandler( + m *mux.Router, + r *render.Renderer, + ah *auth.Handler, +) { if m == nil { m = router.Auth(nil) } + // just the form m.Get(router.AuthFallbackSignInForm).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) { return map[string]interface{}{ csrf.TemplateTag: csrf.TemplateField(req), @@ -29,8 +35,7 @@ func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler { })) // hook up the auth handler to the router - m.Get(router.AuthFallbackSignIn).HandlerFunc(a.Authorize) - m.Get(router.AuthFallbackSignOut).HandlerFunc(a.Logout) + m.Get(router.AuthFallbackSignIn).HandlerFunc(ah.Authorize) - return m + m.Get(router.AuthSignOut).HandlerFunc(ah.Logout) } diff --git a/web/handlers/auth/withssb.go b/web/handlers/auth/withssb.go new file mode 100644 index 0000000..e3c08d7 --- /dev/null +++ b/web/handlers/auth/withssb.go @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + "encoding/base64" + "encoding/gob" + "fmt" + "net/http" + "time" + + "github.com/gorilla/mux" + "github.com/gorilla/sessions" + "go.cryptoscope.co/muxrpc/v2" + "go.mindeco.de/http/render" + + "github.com/ssb-ngi-pointer/go-ssb-room/internal/network" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors" + "github.com/ssb-ngi-pointer/go-ssb-room/web/router" + refs "go.mindeco.de/ssb-refs" +) + +// WithSSBHandler implements the oauth-like challenge/response dance described in +// https://ssb-ngi-pointer.github.io/rooms2/#sign-in-with-ssb +type WithSSBHandler struct { + roomID refs.FeedRef + + membersdb roomdb.MembersService + aliasesdb roomdb.AliasesService + sessiondb roomdb.AuthWithSSBService + + cookieStore sessions.Store + + endpoints network.Endpoints +} + +func NewWithSSBHandler( + m *mux.Router, + r *render.Renderer, + roomID refs.FeedRef, + endpoints network.Endpoints, + aliasDB roomdb.AliasesService, + membersDB roomdb.MembersService, + sessiondb roomdb.AuthWithSSBService, + cookies sessions.Store, +) *WithSSBHandler { + + var ssb WithSSBHandler + ssb.roomID = roomID + ssb.aliasesdb = aliasDB + ssb.membersdb = membersDB + ssb.endpoints = endpoints + ssb.sessiondb = sessiondb + ssb.cookieStore = cookies + + m.Get(router.AuthWithSSBSignIn).HandlerFunc(r.HTML("auth/withssb_sign_in.tmpl", ssb.login)) + + return &ssb +} + +func (h WithSSBHandler) login(w http.ResponseWriter, req *http.Request) (interface{}, error) { + queryParams := req.URL.Query() + + var clientReq signinwithssb.ClientRequest + clientReq.ServerID = h.roomID // fill in the server + + // validate and update client challenge + cc := queryParams.Get("cc") + if _, err := signinwithssb.DecodeChallengeString(cc); err != nil { + return nil, weberrors.ErrBadRequest{Where: "client-challenge", Details: err} + } + clientReq.ClientChallenge = cc + + // check who the client is + var client refs.FeedRef + if cid := queryParams.Get("cid"); cid != "" { + parsed, err := refs.ParseFeedRef(cid) + if err != nil { + return nil, weberrors.ErrBadRequest{Where: "cid", Details: err} + } + client = *parsed + } else { + alias, err := h.aliasesdb.Resolve(req.Context(), queryParams.Get("alias")) + if err != nil { + return nil, weberrors.ErrBadRequest{Where: "alias", Details: err} + } + client = alias.Feed + } + + // check that we have that member + member, err := h.membersdb.GetByFeed(req.Context(), client) + if err != nil { + if err == roomdb.ErrNotFound { + return nil, weberrors.ErrForbidden{Details: fmt.Errorf("sign-in: client isnt a member")} + } + return nil, err + } + clientReq.ClientID = client + + // get the connected client for that member + edp, connected := h.endpoints.GetEndpointFor(client) + if !connected { + return nil, weberrors.ErrForbidden{Details: fmt.Errorf("sign-in: client not connected to room")} + } + + // roll a Challenge from the server + sc := signinwithssb.GenerateChallenge() + clientReq.ServerChallenge = sc + + ctx, cancel := context.WithTimeout(req.Context(), 1*time.Minute) + defer cancel() + + // request the signed solution over muxrpc + var solution string + err = edp.Async(ctx, &solution, muxrpc.TypeString, muxrpc.Method{"httpAuth", "requestSolution"}, sc, cc) + if err != nil { + return nil, err + } + + // decode and validate the response + solutionBytes, err := base64.URLEncoding.DecodeString(solution) + if err != nil { + return nil, err + } + + if !clientReq.Validate(solutionBytes) { + return nil, fmt.Errorf("sign-in with ssb: validation of client solution failed") + } + + // create a session for invalidation + tok, err := h.sessiondb.CreateToken(req.Context(), member.ID) + if err != nil { + return nil, err + } + + session, err := h.cookieStore.Get(req, siwssbSessionName) + if err != nil { + return nil, err + } + + session.Values[memberToken] = tok + session.Values[userTimeout] = time.Now().Add(lifetime) + if err := session.Save(req, w); err != nil { + return nil, err + } + + return "you are now logged in!", nil +} + +// custom sessionKey type to prevent collision +type sessionKey uint + +func init() { + // need to register our Key with gob so gorilla/sessions can (de)serialize it + gob.Register(memberToken) + gob.Register(time.Time{}) +} + +const ( + siwssbSessionName = "AuthWithSSBSession" + + memberToken sessionKey = iota + userTimeout +) + +const lifetime = time.Hour * 24 + +// Authenticate calls the next unless AuthenticateRequest returns an error +func (h WithSSBHandler) Authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if _, err := h.AuthenticateRequest(r); err != nil { + // TODO: render.Error + http.Error(w, weberrors.ErrNotAuthorized.Error(), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + +// AuthenticateRequest uses the passed request to load and return the session data that was stored previously. +// If it is invalid or there is no session, it will return ErrNotAuthorized. +// Otherwise it will return the member ID that belongs to the session. +func (h WithSSBHandler) AuthenticateRequest(r *http.Request) (*roomdb.Member, error) { + session, err := h.cookieStore.Get(r, siwssbSessionName) + if err != nil { + return nil, err + } + + if session.IsNew { + return nil, weberrors.ErrNotAuthorized + } + + tokenVal, ok := session.Values[memberToken] + if !ok { + return nil, weberrors.ErrNotAuthorized + } + + t, ok := session.Values[userTimeout] + if !ok { + return nil, weberrors.ErrNotAuthorized + } + + tout, ok := t.(time.Time) + if !ok { + return nil, weberrors.ErrNotAuthorized + } + + if time.Now().After(tout) { + return nil, weberrors.ErrNotAuthorized + } + + token, ok := tokenVal.(string) + if !ok { + return nil, weberrors.ErrNotAuthorized + } + + memberID, err := h.sessiondb.CheckToken(r.Context(), token) + if err != nil { + return nil, err + } + + member, err := h.membersdb.GetByID(r.Context(), memberID) + if err != nil { + return nil, err + } + + return &member, nil +} + +// Logout destroys the session data and updates the cookie with an invalidated one. +func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) { + session, err := h.cookieStore.Get(r, siwssbSessionName) + if err != nil { + // TODO: render.Error + http.Error(w, err.Error(), http.StatusInternalServerError) + // ah.errorHandler(w, r, err, http.StatusInternalServerError) + return + } + + session.Values[userTimeout] = time.Now().Add(-lifetime) + session.Options.MaxAge = -1 + if err := session.Save(r, w); err != nil { + // TODO: render.Error + http.Error(w, err.Error(), http.StatusInternalServerError) + // ah.errorHandler(w, r, err, http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) + return +} diff --git a/web/handlers/auth_test.go b/web/handlers/auth_test.go index ecb4df6..673b763 100644 --- a/web/handlers/auth_test.go +++ b/web/handlers/auth_test.go @@ -2,6 +2,8 @@ package handlers import ( "bytes" + "context" + "encoding/base64" "net/http" "net/http/cookiejar" "net/url" @@ -9,7 +11,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.cryptoscope.co/muxrpc/v2" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb" + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + "github.com/ssb-ngi-pointer/go-ssb-room/web" "github.com/ssb-ngi-pointer/go-ssb-room/web/router" "github.com/ssb-ngi-pointer/go-ssb-room/web/webassert" refs "go.mindeco.de/ssb-refs" @@ -53,7 +60,7 @@ func TestFallbackAuth(t *testing.T) { ts := setup(t) a, r := assert.New(t), require.New(t) - // very cheap client session + // very cheap "browser" client session jar, err := cookiejar.New(nil) r.NoError(err) @@ -112,6 +119,7 @@ func TestFallbackAuth(t *testing.T) { sessionCookie := resp.Result().Cookies() jar.SetCookies(signInURL, sessionCookie) + // now request the protected dashboard page dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL() r.Nil(err) dashboardURL.Host = "localhost" @@ -169,3 +177,196 @@ func TestFallbackAuth(t *testing.T) { {"#roomCount", "AdminRoomCountPlural"}, }) } + +func TestAuthWithSSBNotConnected(t *testing.T) { + ts := setup(t) + a, r := assert.New(t), require.New(t) + + // the client is a member but not connected right now + ts.MembersDB.GetByFeedReturns(roomdb.Member{ID: 1234, Nickname: "test-member"}, nil) + ts.MockedEndpoints.GetEndpointForReturns(nil, false) + + client, err := keys.NewKeyPair(nil) + r.NoError(err) + + cc := signinwithssb.GenerateChallenge() + + urlTo := web.NewURLTo(ts.Router) + + signInStartURL := urlTo(router.AuthWithSSBSignIn, + "cid", client.Feed.Ref(), + "challenge", cc, + ) + r.NotNil(signInStartURL) + + t.Log(signInStartURL.String()) + doc, resp := ts.Client.GetHTML(signInStartURL.String()) + a.Equal(http.StatusInternalServerError, resp.Code) // TODO: StatusForbidden + + webassert.Localized(t, doc, []webassert.LocalizedElement{ + // {"#welcome", "AuthWithSSBWelcome"}, + // {"title", "AuthWithSSBTitle"}, + }) +} + +func TestAuthWithSSBNotAllowed(t *testing.T) { + ts := setup(t) + a, r := assert.New(t), require.New(t) + + // the client isnt a member + ts.MembersDB.GetByFeedReturns(roomdb.Member{}, roomdb.ErrNotFound) + ts.MockedEndpoints.GetEndpointForReturns(nil, false) + + client, err := keys.NewKeyPair(nil) + r.NoError(err) + + cc := signinwithssb.GenerateChallenge() + + urlTo := web.NewURLTo(ts.Router) + + signInStartURL := urlTo(router.AuthWithSSBSignIn, + "cid", client.Feed.Ref(), + "challenge", cc, + ) + r.NotNil(signInStartURL) + + t.Log(signInStartURL.String()) + doc, resp := ts.Client.GetHTML(signInStartURL.String()) + a.Equal(http.StatusInternalServerError, resp.Code) // TODO: StatusForbidden + + webassert.Localized(t, doc, []webassert.LocalizedElement{ + // {"#welcome", "AuthWithSSBWelcome"}, + // {"title", "AuthWithSSBTitle"}, + }) +} + +func TestAuthWithSSBHasClient(t *testing.T) { + ts := setup(t) + a, r := assert.New(t), require.New(t) + + // very cheap "browser" client session + jar, err := cookiejar.New(nil) + r.NoError(err) + + // the request to be signed later + var req signinwithssb.ClientRequest + req.ServerID = ts.NetworkInfo.RoomID + + // the keypair for our client + testMember := roomdb.Member{ID: 1234, Nickname: "test-member"} + client, err := keys.NewKeyPair(nil) + r.NoError(err) + testMember.PubKey = client.Feed + + // setup the mocked database + ts.MembersDB.GetByFeedReturns(testMember, nil) + ts.AuthWithSSB.CreateTokenReturns("abcdefgh", nil) + ts.AuthWithSSB.CheckTokenReturns(testMember.ID, nil) + ts.MembersDB.GetByIDReturns(testMember, nil) + + // fill the basic infos of the request + req.ClientID = client.Feed + + // this is our fake "connected" client + var edp muxrpc.FakeEndpoint + + // setup a mocked muxrpc call that asserts the arguments and returns the needed signature + edp.AsyncCalls(func(_ context.Context, ret interface{}, encoding muxrpc.RequestEncoding, method muxrpc.Method, args ...interface{}) error { + a.Equal(muxrpc.TypeString, encoding) + a.Equal("httpAuth.requestSolution", method.String()) + + r.Len(args, 2, "expected two args") + + serverChallenge, ok := args[0].(string) + r.True(ok, "argument[0] is not a string: %T", args[0]) + a.NotEqual("", serverChallenge) + // update the challenge + req.ServerChallenge = serverChallenge + + clientChallenge, ok := args[1].(string) + r.True(ok, "argument[1] is not a string: %T", args[1]) + a.Equal(req.ClientChallenge, clientChallenge) + + strptr, ok := ret.(*string) + r.True(ok, "return is not a string pointer: %T", ret) + + // sign the request now that we have the sc + clientSig := req.Sign(client.Pair.Secret) + + *strptr = base64.URLEncoding.EncodeToString(clientSig) + return nil + }) + + // setup the fake client endpoint + ts.MockedEndpoints.GetEndpointForReturns(&edp, true) + + cc := signinwithssb.GenerateChallenge() + // update the challenge + req.ClientChallenge = cc + + // prepare the url + signInStartURL := web.NewURLTo(ts.Router)(router.AuthWithSSBSignIn, + "cid", client.Feed.Ref(), + "cc", cc, + ) + signInStartURL.Host = "localhost" + signInStartURL.Scheme = "https" + + r.NotNil(signInStartURL) + + t.Log(signInStartURL.String()) + doc, resp := ts.Client.GetHTML(signInStartURL.String()) + a.Equal(http.StatusOK, resp.Code) + + webassert.Localized(t, doc, []webassert.LocalizedElement{ + // {"#welcome", "AuthWithSSBWelcome"}, + // {"title", "AuthWithSSBTitle"}, + }) + + // analyse the endpoints call + a.Equal(1, ts.MockedEndpoints.GetEndpointForCallCount()) + edpRef := ts.MockedEndpoints.GetEndpointForArgsForCall(0) + a.Equal(client.Feed.Ref(), edpRef.Ref()) + + // check the mock was called + a.Equal(1, edp.AsyncCallCount()) + + // check that we have a new cookie + sessionCookie := resp.Result().Cookies() + r.True(len(sessionCookie) > 0, "expecting one cookie!") + jar.SetCookies(signInStartURL, sessionCookie) + + // now request the protected dashboard page + dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL() + r.Nil(err) + dashboardURL.Host = "localhost" + dashboardURL.Scheme = "https" + + var sessionHeader = http.Header(map[string][]string{}) + cs := jar.Cookies(dashboardURL) + + r.True(len(cs) > 0, "expecting one cookie!") + for _, c := range cs { + theCookie := c.String() + a.NotEqual("", theCookie, "should have a new cookie") + sessionHeader.Add("Cookie", theCookie) + } + + durl := dashboardURL.String() + t.Log(durl) + + // update headers + ts.Client.ClearHeaders() + ts.Client.SetHeaders(sessionHeader) + + html, resp := ts.Client.GetHTML(durl) + if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") { + t.Log(html.Find("body").Text()) + } + + webassert.Localized(t, html, []webassert.LocalizedElement{ + {"#welcome", "AdminDashboardWelcome"}, + {"title", "AdminDashboardTitle"}, + }) + +} diff --git a/web/handlers/http.go b/web/handlers/http.go index 7b96823..c8a74b1 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -11,8 +11,6 @@ import ( "strconv" "time" - refs "go.mindeco.de/ssb-refs" - "github.com/gorilla/csrf" "github.com/gorilla/sessions" "github.com/russross/blackfriday/v2" @@ -20,6 +18,7 @@ import ( "go.mindeco.de/http/render" "go.mindeco.de/logging" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/network" "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/roomstate" @@ -29,6 +28,7 @@ import ( "github.com/ssb-ngi-pointer/go-ssb-room/web/i18n" "github.com/ssb-ngi-pointer/go-ssb-room/web/members" "github.com/ssb-ngi-pointer/go-ssb-room/web/router" + refs "go.mindeco.de/ssb-refs" ) var HTMLTemplates = []string{ @@ -46,6 +46,7 @@ var HTMLTemplates = []string{ type Databases struct { Aliases roomdb.AliasesService AuthFallback roomdb.AuthFallbackService + AuthWithSSB roomdb.AuthWithSSBService DeniedKeys roomdb.DeniedKeysService Invites roomdb.InvitesService Notices roomdb.NoticesService @@ -69,8 +70,8 @@ func New( repo repo.Interface, netInfo NetworkInfo, roomState *roomstate.Manager, + roomEndpoints network.Endpoints, dbs Databases, - ) (http.Handler, error) { m := router.CompleteApp() @@ -147,7 +148,7 @@ func New( return nil, err } - store := &sessions.CookieStore{ + cookieStore := &sessions.CookieStore{ Codecs: cookieCodec, Options: &sessions.Options{ Path: "/", @@ -197,8 +198,8 @@ func New( }, nil }) - a, err := auth.NewHandler(dbs.AuthFallback, - auth.SetStore(store), + authWithPassword, err := auth.NewHandler(dbs.AuthFallback, + auth.SetStore(cookieStore), auth.SetErrorHandler(authErrH), auth.SetNotAuthorizedHandler(notAuthorizedH), auth.SetLifetime(2*time.Hour), // TODO: configure @@ -225,8 +226,21 @@ func New( // TODO: explain problem between gorilla/mux named routers and authentication mainMux := &http.ServeMux{} - // hookup handlers to the router - roomsAuth.Handler(m, r, a) + // start hooking up handlers to the router + + authWithSSB := roomsAuth.NewWithSSBHandler( + m, + r, + netInfo.RoomID, + roomEndpoints, + dbs.Aliases, + dbs.Members, + dbs.AuthWithSSB, + cookieStore, + ) + + // just hooks up the router to the handler + roomsAuth.NewFallbackPasswordHandler(m, r, authWithPassword) adminHandler := admin.Handler( netInfo.Domain, @@ -241,7 +255,7 @@ func New( PinnedNotices: dbs.PinnedNotices, }, ) - mainMux.Handle("/admin/", a.Authenticate(adminHandler)) + mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler)) m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) { notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB") @@ -297,7 +311,7 @@ func New( // apply HTTP middleware middlewares := []func(http.Handler) http.Handler{ logging.InjectHandler(logger), - members.ContextInjecter(dbs.Members, a), + members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB), CSRF, } diff --git a/web/handlers/invites.go b/web/handlers/invites.go index 96c136d..93fec4b 100644 --- a/web/handlers/invites.go +++ b/web/handlers/invites.go @@ -49,12 +49,11 @@ func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (inter } alias := req.FormValue("alias") - token := req.FormValue("token") newMember, err := refs.ParseFeedRef(req.FormValue("new_member")) if err != nil { - return nil, weberrors.ErrBadRequest{Where: "form data", Details: err} + return nil, weberrors.ErrBadRequest{Where: "new_member", Details: err} } inv, err := h.invites.Consume(req.Context(), token, *newMember) diff --git a/web/handlers/setup_test.go b/web/handlers/setup_test.go index 72a5b2a..b5bb49b 100644 --- a/web/handlers/setup_test.go +++ b/web/handlers/setup_test.go @@ -18,6 +18,7 @@ import ( "go.mindeco.de/logging/logtest" refs "go.mindeco.de/ssb-refs" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/network/mocked" "github.com/ssb-ngi-pointer/go-ssb-room/internal/repo" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb" @@ -34,6 +35,7 @@ type testSession struct { // mocked dbs AuthDB *mockdb.FakeAuthWithSSBService AuthFallbackDB *mockdb.FakeAuthFallbackService + AuthWithSSB *mockdb.FakeAuthWithSSBService AliasesDB *mockdb.FakeAliasesService MembersDB *mockdb.FakeMembersService InvitesDB *mockdb.FakeInvitesService @@ -42,6 +44,8 @@ type testSession struct { RoomState *roomstate.Manager + MockedEndpoints *mocked.FakeEndpoints + NetworkInfo NetworkInfo } @@ -63,6 +67,7 @@ func setup(t *testing.T) *testSession { ts.AuthDB = new(mockdb.FakeAuthWithSSBService) ts.AuthFallbackDB = new(mockdb.FakeAuthFallbackService) + ts.AuthWithSSB = new(mockdb.FakeAuthWithSSBService) ts.AliasesDB = new(mockdb.FakeAliasesService) ts.MembersDB = new(mockdb.FakeMembersService) ts.InvitesDB = new(mockdb.FakeInvitesService) @@ -74,6 +79,8 @@ func setup(t *testing.T) *testSession { ts.PinnedDB.GetReturns(defaultNotice, nil) ts.NoticeDB = new(mockdb.FakeNoticesService) + ts.MockedEndpoints = new(mocked.FakeEndpoints) + ts.NetworkInfo = NetworkInfo{ Domain: "localhost", PortMUXRPC: 8008, @@ -96,9 +103,11 @@ func setup(t *testing.T) *testSession { testRepo, ts.NetworkInfo, ts.RoomState, + ts.MockedEndpoints, Databases{ Aliases: ts.AliasesDB, AuthFallback: ts.AuthFallbackDB, + AuthWithSSB: ts.AuthWithSSB, Members: ts.MembersDB, Invites: ts.InvitesDB, Notices: ts.NoticeDB, diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index 88eb9c2..fc26a25 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -19,6 +19,9 @@ AuthFallbackTitle = "The place of last resort" AuthSignIn = "Sign in" AuthSignOut = "Sign out" +AuthWithSSBTitle = "Sign-in with SSB" +AuthWithSSBWelcome = "If you have a compatible device/application, you can sign-in here without a password." + AdminDashboardWelcome = "Welcome to your dashboard" AdminDashboardTitle = "Room Admin Dashboard" diff --git a/web/members/helper.go b/web/members/helper.go index 7ed7ab7..f20c96f 100644 --- a/web/members/helper.go +++ b/web/members/helper.go @@ -5,45 +5,80 @@ import ( "context" "net/http" - "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" "go.mindeco.de/http/auth" + "go.mindeco.de/http/render" + + "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" + weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors" + authWithSSB "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth" ) type roomMemberContextKeyType string var roomMemberContextKey roomMemberContextKeyType = "ssb:room:httpcontext:member" +type Middleware func(next http.Handler) http.Handler + +// AuthenticateFromContext calls the next http handler if there is a member stored in the context +// otherwise it will call r.Error +func AuthenticateFromContext(r *render.Renderer) Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if FromContext(req.Context()) == nil { + r.Error(w, req, http.StatusUnauthorized, weberrors.ErrBadRequest{}) + return + } + next.ServeHTTP(w, req) + }) + } +} + // FromContext returns the member or nil if not logged in func FromContext(ctx context.Context) *roomdb.Member { v := ctx.Value(roomMemberContextKey) - m, ok := v.(roomdb.Member) + m, ok := v.(*roomdb.Member) if !ok { return nil } - return &m + return m } // ContextInjecter returns middleware for injecting a member into the context of the request. // Retreive it using FromContext(ctx) -func ContextInjecter(mdb roomdb.MembersService, a *auth.Handler) func(http.Handler) http.Handler { +func ContextInjecter(mdb roomdb.MembersService, withPassword *auth.Handler, withSSB *authWithSSB.WithSSBHandler) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - v, err := a.AuthenticateRequest(req) - if err != nil { - next.ServeHTTP(w, req) - return + var ( + member *roomdb.Member + + errWithPassword, errWithSSB error + ) + + v, errWithPassword := withPassword.AuthenticateRequest(req) + if errWithPassword == nil { + mid, ok := v.(int64) + if !ok { + next.ServeHTTP(w, req) + return + } + + m, err := mdb.GetByID(req.Context(), mid) + if err != nil { + next.ServeHTTP(w, req) + return + } + member = &m } - mid, ok := v.(int64) - if !ok { - next.ServeHTTP(w, req) - return + m, errWithSSB := withSSB.AuthenticateRequest(req) + if errWithSSB == nil { + member = m } - member, err := mdb.GetByID(req.Context(), mid) - if err != nil { + // if both methods failed, don't update the context + if errWithPassword != nil && errWithSSB != nil { next.ServeHTTP(w, req) return } diff --git a/web/members/testing.go b/web/members/testing.go index 85c646f..696fc51 100644 --- a/web/members/testing.go +++ b/web/members/testing.go @@ -14,7 +14,7 @@ import ( func MiddlewareForTests(m roomdb.Member) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx := context.WithValue(req.Context(), roomMemberContextKey, m) + ctx := context.WithValue(req.Context(), roomMemberContextKey, &m) next.ServeHTTP(w, req.WithContext(ctx)) }) } diff --git a/web/router/auth.go b/web/router/auth.go index 9fcc13f..d74e3d0 100644 --- a/web/router/auth.go +++ b/web/router/auth.go @@ -8,10 +8,10 @@ import "github.com/gorilla/mux" const ( AuthFallbackSignInForm = "auth:fallback:signin:form" AuthFallbackSignIn = "auth:fallback:signin" - AuthFallbackSignOut = "auth:fallback:logout" - AuthWithSSBSignIn = "auth:ssb:signin" - AuthWithSSBSignOut = "auth:ssb:logout" + AuthWithSSBSignIn = "auth:ssb:signin" + + AuthSignOut = "auth:logout" ) // NewSignin constructs a mux.Router containing the routes for sign-in and -out @@ -23,10 +23,10 @@ func Auth(m *mux.Router) *mux.Router { // register fallback m.Path("/fallback/signin").Methods("GET").Name(AuthFallbackSignInForm) m.Path("/fallback/signin").Methods("POST").Name(AuthFallbackSignIn) - m.Path("/fallback/logout").Methods("GET").Name(AuthFallbackSignOut) m.Path("/withssb/signin").Methods("GET").Name(AuthWithSSBSignIn) - m.Path("/withssb/logout").Methods("GET").Name(AuthWithSSBSignOut) + + m.Path("/logout").Methods("GET").Name(AuthSignOut) return m } diff --git a/web/templates/auth/withssb_sign_in.tmpl b/web/templates/auth/withssb_sign_in.tmpl new file mode 100644 index 0000000..3931fbb --- /dev/null +++ b/web/templates/auth/withssb_sign_in.tmpl @@ -0,0 +1,9 @@ +{{ define "title" }}{{i18n "AuthWithSSBTitle"}}{{ end }} +{{ define "content" }} + +
+
{{.}}
+
+{{end}} \ No newline at end of file diff --git a/web/templates/base.tmpl b/web/templates/base.tmpl index f28a75c..2b88741 100644 --- a/web/templates/base.tmpl +++ b/web/templates/base.tmpl @@ -34,7 +34,7 @@ {{$user.Nickname}} {{i18n "AuthSignOut"}}