external login
All checks were successful
docker / build-docker (push) Successful in 3m29s

This commit is contained in:
royalcat 2024-12-25 17:20:49 +03:00
parent 65b1d40da2
commit 4b60194897
15 changed files with 333 additions and 126 deletions

View file

@ -8,6 +8,7 @@ import (
"github.com/ilyakaznacheev/cleanenv"
"github.com/royalcat/konfa-server/pkg/uuid"
"github.com/royalcat/konfa-server/src/auth"
"github.com/royalcat/konfa-server/src/konfa"
"github.com/royalcat/konfa-server/src/proto"
chatv1 "github.com/royalcat/konfa-server/src/proto/konfa/chat/v1"
@ -36,7 +37,19 @@ func main() {
srv := konfa.NewService(db, dbpool)
grpcServer := grpc.NewServer(grpc.UnaryInterceptor(proto.Authenticate))
authen, err := auth.NewAuthenticator(ctx, db, auth.AuthenticatorConfig{
Issuer: "https://sso.konfach.ru/realms/konfach",
ClientID: "konfa",
ClientSecret: "UqeaMowRXcGULkAepr0EAEUfE82OjY72",
})
if err != nil {
panic(err)
}
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(authen.UnaryAuthenticate),
grpc.StreamInterceptor(authen.StreamAuthenticate),
)
chatv1.RegisterChatServiceServer(grpcServer, proto.NewChatService(srv))
serverv1.RegisterServerServiceServer(grpcServer, proto.NewServerService(srv))
@ -80,7 +93,7 @@ func createKonfach(ctx context.Context, srv *konfa.Service) (uuid.UUID, uuid.UUI
var chanID uuid.UUID
channels, err := srv.ListChannelsOnServer(ctx, serverID)
channels, err := srv.ListTextChannelsOnServer(ctx, serverID)
if err != nil {
return uuid.Nil, uuid.Nil, err
}
@ -90,7 +103,7 @@ func createKonfach(ctx context.Context, srv *konfa.Service) (uuid.UUID, uuid.UUI
}
}
if chanID == uuid.Nil {
chanID, err = srv.CreateChannel(ctx, serverID, "general")
chanID, err = srv.CreateTextChannel(ctx, serverID, "general")
if err != nil {
return uuid.Nil, uuid.Nil, fmt.Errorf("failed to create channel: %w", err)
}

13
go.mod
View file

@ -24,9 +24,12 @@ require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/coreos/go-oidc/v3 v3.11.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -42,13 +45,19 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect
github.com/zitadel/logging v0.6.1 // indirect
github.com/zitadel/oidc v1.13.5 // indirect
github.com/zitadel/oidc/v3 v3.33.1 // indirect
github.com/zitadel/schema v1.3.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
@ -56,10 +65,12 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)

29
go.sum
View file

@ -10,6 +10,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cskr/pubsub/v2 v2.0.2 h1:395hhPXEsyI1b+5nfj+s5Q3gdxpg0jsWd3t/QAdmU1Y=
github.com/cskr/pubsub/v2 v2.0.2/go.mod h1:XYuiN8dhcXTCzQDa5SH4+B3zLso94FTwAk0maAEGJJw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -31,6 +33,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -48,6 +54,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -97,6 +105,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -117,6 +127,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -141,6 +153,14 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI=
github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo=
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
github.com/zitadel/oidc v1.13.5 h1:7jhh68NGZitLqwLiVU9Dtwa4IraJPFF1vS+4UupO93U=
github.com/zitadel/oidc v1.13.5/go.mod h1:rHs1DhU3Sv3tnI6bQRVlFa3u0lCwtR7S21WHY+yXgPA=
github.com/zitadel/oidc/v3 v3.33.1 h1:e3w9PDV0Mh50/ZiJWtzyT0E4uxJ6RXll+hqVDnqGbTU=
github.com/zitadel/oidc/v3 v3.33.1/go.mod h1:zkoZ1Oq6CweX3BaLrftLEGCs6YK6zDpjjVGZrP10AWU=
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
@ -159,14 +179,21 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
@ -176,6 +203,8 @@ google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

120
src/auth/auth.go Normal file
View file

@ -0,0 +1,120 @@
package auth
import (
"context"
"database/sql"
"errors"
"github.com/royalcat/konfa-server/pkg/uuid"
"github.com/royalcat/konfa-server/src/store"
"github.com/uptrace/bun"
"github.com/zitadel/oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
errInvalidToken = status.Errorf(codes.Unauthenticated, "invalid token")
)
type AuthenticatorConfig struct {
Issuer string
ClientID string
ClientSecret string
}
type Authenticator struct {
provider rs.ResourceServer
db *bun.DB
}
func NewAuthenticator(ctx context.Context, db *bun.DB, acfg AuthenticatorConfig) (*Authenticator, error) {
provider, err := rs.NewResourceServerClientCredentials(ctx, acfg.Issuer, acfg.ClientID, acfg.ClientSecret)
if err != nil {
return nil, err
}
return &Authenticator{
provider: provider,
db: db,
}, nil
}
func (a *Authenticator) authorize(ctx context.Context, token string) (context.Context, error) {
// var claims oidc.AccessTokenClaims
// _, err := oidc.ParseToken(token, &claims)
// if err != nil {
// return nil, err
// }
resp, err := rs.Introspect[*oidc.IntrospectionResponse](ctx, a.provider, token)
if err != nil {
return nil, err
}
user, err := a.loginWithExternal(ctx, resp)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, ctxUserKey, &user)
return ctx, nil
}
func (a *Authenticator) loginWithExternal(ctx context.Context, resp *oidc.IntrospectionResponse) (store.User, error) {
var user store.User
err := a.db.NewSelect().
Model(&user).
Join("JOIN external_login ON \"user\".\"id\" = \"external_login\".\"user_id\"").
Where("issuer = ?", resp.Issuer).
Where("subject = ?", resp.Subject).
Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return a.createUserFromExternal(ctx, resp)
}
return store.User{}, err
}
return user, nil
}
func (a *Authenticator) createUserFromExternal(ctx context.Context, resp *oidc.IntrospectionResponse) (store.User, error) {
user := store.User{
ID: uuid.New(),
Username: resp.Username,
}
err := a.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
_, err := tx.NewInsert().
Model(&user).
Exec(ctx)
if err != nil {
return err
}
_, err = tx.NewInsert().
Model(&store.ExternalLogin{
UserID: user.ID,
Issuer: resp.Issuer,
Subject: resp.Subject,
}).
Exec(ctx)
if err != nil {
return err
}
return nil
})
if err != nil {
return store.User{}, err
}
return user, nil
}

15
src/auth/ctx.go Normal file
View file

@ -0,0 +1,15 @@
package auth
import (
"context"
"github.com/royalcat/konfa-server/src/store"
)
type ctxKey string
const ctxUserKey ctxKey = "user"
func CtxGetUser(ctx context.Context) *store.User {
return ctx.Value(ctxUserKey).(*store.User)
}

66
src/auth/grpc.go Normal file
View file

@ -0,0 +1,66 @@
package auth
import (
"context"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func (a *Authenticator) UnaryAuthenticate(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errMissingMetadata
}
token := grpcExtractToken(md["authorization"])
if token == "" {
return nil, errInvalidToken
}
ctx, err := a.authorize(ctx, token)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
func (a *Authenticator) StreamAuthenticate(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return errMissingMetadata
}
token := grpcExtractToken(md["authorization"])
if token == "" {
return errInvalidToken
}
ctx, err := a.authorize(ctx, token)
if err != nil {
return err
}
return handler(srv, newWrappedStream(ctx, ss))
}
func grpcExtractToken(authorization []string) string {
if len(authorization) < 1 {
return ""
}
return strings.TrimPrefix(authorization[0], "Bearer ")
}
type wrappedStreamContext struct {
ctx context.Context
grpc.ServerStream
}
func newWrappedStream(ctx context.Context, s grpc.ServerStream) grpc.ServerStream {
return &wrappedStreamContext{ctx: ctx, ServerStream: s}
}

View file

@ -7,7 +7,7 @@ import (
"github.com/royalcat/konfa-server/src/store"
)
func (c *Service) CreateChannel(ctx context.Context, serverID uuid.UUID, name string) (uuid.UUID, error) {
func (c *Service) CreateTextChannel(ctx context.Context, serverID uuid.UUID, name string) (uuid.UUID, error) {
channel := store.TextChannel{
ID: uuid.New(),
ServerID: serverID,
@ -34,7 +34,7 @@ func (c *Service) GetChannel(ctx context.Context, serverID uuid.UUID, channelID
return channel, err
}
func (c *Service) ListChannelsOnServer(ctx context.Context, serverID uuid.UUID) ([]store.TextChannel, error) {
func (c *Service) ListTextChannelsOnServer(ctx context.Context, serverID uuid.UUID) ([]store.TextChannel, error) {
var channels []store.TextChannel
err := c.db.NewSelect().
Model(&channels).

View file

@ -34,12 +34,3 @@ func (c *Service) ListServers(ctx context.Context) ([]store.Server, error) {
Scan(ctx)
return servers, err
}
func (c *Service) ListTextChannels(ctx context.Context, serverID uuid.UUID) ([]store.TextChannel, error) {
var channels []store.TextChannel
err := c.db.NewSelect().
Model(&channels).
Where("server_id = ?", serverID).
Scan(ctx)
return channels, err
}

26
src/konfa/user.go Normal file
View file

@ -0,0 +1,26 @@
package konfa
import (
"context"
"github.com/royalcat/konfa-server/pkg/uuid"
"github.com/royalcat/konfa-server/src/store"
"github.com/uptrace/bun"
)
type Users struct {
db *bun.DB
}
func NewUsers(db *bun.DB) *Users {
return &Users{db: db}
}
func (u *Users) GetUser(ctx context.Context, id uuid.UUID) (store.User, error) {
var user store.User
err := u.db.NewSelect().
Model(&user).
Where("id = ?", id).
Scan(ctx)
return user, err
}

View file

@ -1,97 +0,0 @@
package proto
import (
"context"
"flag"
"strings"
"github.com/royalcat/konfa-server/pkg/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
errInvalidToken = status.Errorf(codes.Unauthenticated, "invalid token")
)
var port = flag.Int("port", 50051, "the port to serve on")
// func main() {
// flag.Parse()
// fmt.Printf("server starting on port %d...\n", *port)
// cert, err := tls.LoadX509KeyPair(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
// if err != nil {
// log.Fatalf("failed to load key pair: %s", err)
// }
// opts := []grpc.ServerOption{
// // The following grpc.ServerOption adds an interceptor for all unary
// // RPCs. To configure an interceptor for streaming RPCs, see:
// // https://godoc.org/google.golang.org/grpc#StreamInterceptor
// grpc.UnaryInterceptor(ensureValidToken),
// // Enable TLS for all incoming connections.
// grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
// }
// s := grpc.NewServer(opts...)
// pb.RegisterEchoServer(s, &ecServer{})
// lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
// if err != nil {
// log.Fatalf("failed to listen: %v", err)
// }
// if err := s.Serve(lis); err != nil {
// log.Fatalf("failed to serve: %v", err)
// }
// }
// valid validates the authorization.
func valid(authorization []string) bool {
if len(authorization) < 1 {
return false
}
token := strings.TrimPrefix(authorization[0], "Bearer ")
// Perform the token validation here. For the sake of this example, the code
// here forgoes any of the usual OAuth2 token validation and instead checks
// for a token matching an arbitrary string.
return token == "some-secret-token"
}
type ctxKey string
const ctxUserKey ctxKey = "user"
// Authenticate ensures a valid token exists within a request's metadata. If
// the token is missing or invalid, the interceptor blocks execution of the
// handler and returns an error. Otherwise, the interceptor invokes the unary
// handler.
func Authenticate(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
// md, ok := metadata.FromIncomingContext(ctx)
// if !ok {
// return nil, errMissingMetadata
// }
// // The keys within metadata.MD are normalized to lowercase.
// // See: https://godoc.org/google.golang.org/grpc/metadata#New
// if !valid(md["authorization"]) {
// return nil, errInvalidToken
// }
var user User
user = User{
ID: uuid.MustFromString("a903b474-26f4-4262-9ba7-97edaa76491f"),
}
ctx = context.WithValue(ctx, ctxUserKey, &user)
// Continue execution of handler after ensuring a valid token.
return handler(ctx, req)
}
type User struct {
ID uuid.UUID
}
func getCtxUser(ctx context.Context) *User {
return ctx.Value(ctxUserKey).(*User)
}

View file

@ -4,6 +4,7 @@ import (
"context"
"github.com/royalcat/konfa-server/pkg/uuid"
"github.com/royalcat/konfa-server/src/auth"
"github.com/royalcat/konfa-server/src/konfa"
chatv1 "github.com/royalcat/konfa-server/src/proto/konfa/chat/v1"
"google.golang.org/grpc"
@ -27,7 +28,7 @@ var _ chatv1.ChatServiceServer = (*ChatService)(nil)
// SendMessage implements chatv1.ChatServiceServer.
func (c *ChatService) SendMessage(ctx context.Context, req *chatv1.SendMessageRequest) (*chatv1.SendMessageResponse, error) {
user := getCtxUser(ctx)
user := auth.CtxGetUser(ctx)
if user == nil {
return nil, ErrUnauthenticated
}

View file

@ -27,7 +27,7 @@ func (s *ServerService) ListServerChannels(ctx context.Context, req *serverv1.Li
return nil, err
}
textChannels, err := s.srv.ListTextChannels(ctx, serverID)
textChannels, err := s.srv.ListTextChannelsOnServer(ctx, serverID)
if err != nil {
return nil, err
}

25
src/store/models_user.go Normal file
View file

@ -0,0 +1,25 @@
package store
import (
"time"
"github.com/royalcat/konfa-server/pkg/uuid"
"github.com/uptrace/bun"
)
type User struct {
bun.BaseModel `bun:"table:user"`
ID uuid.UUID `bun:"id,pk"`
Username string `bun:"username"`
}
type ExternalLogin struct {
bun.BaseModel `bun:"table:external_login"`
ID uuid.UUID `bun:"id,pk"`
UserID uuid.UUID `bun:"user_id"`
Issuer string `bun:"issuer"`
Subject string `bun:"subject"`
CreatedAt time.Time `bun:"created_at"`
}

View file

@ -3,32 +3,28 @@
-- User Table
CREATE TABLE "user" (
id uuid PRIMARY KEY,
username VARCHAR(255) UNIQUE
username VARCHAR(255) NOT NULL UNIQUE
);
-- Server Table
CREATE TABLE "server" (id uuid PRIMARY KEY, "name" VARCHAR(255));
-- TextChannel Table
CREATE TABLE "text_channel" (
id uuid PRIMARY KEY,
server_id uuid,
FOREIGN KEY (server_id) REFERENCES "server"(id),
"name" VARCHAR(255)
id UUID PRIMARY KEY,
server_id UUID NOT NULL REFERENCES "server"(id) ON DELETE CASCADE,
"name" VARCHAR(255) NOT NULL
);
CREATE UNIQUE INDEX unique_text_channel_name ON "text_channel"(server_id, "name");
-- VoiceChannel Table
CREATE TABLE "voice_channel" (
id uuid PRIMARY KEY,
server_id uuid,
FOREIGN KEY (server_id) REFERENCES "server"(id),
"name" VARCHAR(255)
server_id uuid NOT NULL REFERENCES "server"(id) ON DELETE CASCADE,
"name" VARCHAR(255) NOT NULL
);
-- Message Table
CREATE TABLE "message" (
id uuid PRIMARY KEY,
channel_id uuid NOT NULL,
FOREIGN KEY (channel_id) REFERENCES "text_channel"(id),
sender_id uuid NOT NULL,
FOREIGN KEY (sender_id) REFERENCES "user"(id),
channel_id uuid NOT NULL REFERENCES "text_channel"(id) ON DELETE CASCADE,
sender_id uuid NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
content TEXT NOT NULL,
"timestamp" TIMESTAMPTZ NOT NULL DEFAULT now()
);

View file

@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE "external_login" (
"id" uuid PRIMARY KEY,
"user_id" uuid NOT NULL REFERENCES "user" (id) ON DELETE CASCADE,
"issuer" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX external_login_unique_external_id ON "external_login" ("issuer", "subject");
-- +goose StatementEnd