diff --git a/Dockerfile b/Dockerfile index d4888f6..c06ee15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-alpine AS builder +FROM golang:1.25-alpine AS builder WORKDIR /app @@ -11,10 +11,17 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app ./cmd/grpc/ma FROM alpine:latest -WORKDIR /root/ +# Create an unprivileged user and group +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +WORKDIR /home/appuser COPY --from=builder /app/app . COPY --from=builder /app/config/ /etc/app/ +# Change ownership of files to appuser +RUN chown -R appuser:appgroup /home/appuser /etc/app + +USER appuser ENTRYPOINT ["./app"] CMD ["--config /etc/app/local.yaml"] diff --git a/cmd/grpc/main.go b/cmd/grpc/main.go index efb6faa..71fb8cb 100644 --- a/cmd/grpc/main.go +++ b/cmd/grpc/main.go @@ -17,8 +17,8 @@ import ( "github.com/bmstu-itstech/scriptum-back/internal/config" "github.com/bmstu-itstech/scriptum-back/internal/infra/docker" "github.com/bmstu-itstech/scriptum-back/internal/infra/local" - "github.com/bmstu-itstech/scriptum-back/internal/infra/mock" "github.com/bmstu-itstech/scriptum-back/internal/infra/postgres" + "github.com/bmstu-itstech/scriptum-back/internal/infra/sso" "github.com/bmstu-itstech/scriptum-back/internal/infra/watermill" "github.com/bmstu-itstech/scriptum-back/pkg/logs" "github.com/bmstu-itstech/scriptum-back/pkg/server" @@ -38,7 +38,15 @@ func main() { repos := postgres.MustNewRepository(cfg.Postgres, l) runner := docker.MustNewRunner(cfg.Docker, l) storage := local.MustNewStorage(cfg.Storage, l) - mockIAP := mock.NewIsAdminProvider() + + ssoApi, closeFn := sso.MustNewSSOClient(cfg.SSO, l) + + defer func() { + err := closeFn() + if err != nil { + l.Error("failed to close SSO client", slog.String("error", err.Error())) + } + }() jPub, jSub := watermill.NewJobPubSubGoChannels(l) @@ -47,7 +55,7 @@ func main() { BoxRepo: repos, FileReader: storage, FileUploader: storage, - IsAdminProvider: mockIAP, + IsAdminProvider: ssoApi, JobProvider: repos, JobPublisher: jPub, JobRepository: repos, diff --git a/config/local.yaml b/config/local.yaml index 9a8df5e..c6ea94d 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -7,3 +7,8 @@ logging: storage: base_path: "uploads" + +sso: + host: "localhost" + port: "44044" + app_id: 1 diff --git a/easyp.lock b/easyp.lock index 8c69247..2303fd7 100644 --- a/easyp.lock +++ b/easyp.lock @@ -1,2 +1,2 @@ -github.com/googleapis/googleapis common-protos-1_3_1 h1:s8d5N7+VjksySahU4JpUGSju5SqkmQNtweRfsin9SyM= -github.com/grpc-ecosystem/grpc-gateway v0.0.0-20251127211339-d2d5e587243b4254ac96b76ce19636e0fd734a5e h1:qihtPAOYuB5MmdtIx/0GX3DZrVBCp/kZz2iolgsuOys= +github.com/googleapis/googleapis common-protos-1_3_1 h1:TsE+JW4H/zsB0phfwMO1DragQ4oaGfoVuGHV5aGfn2I= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20251127211339-d2d5e587243b4254ac96b76ce19636e0fd734a5e h1:48F3p4F2740v+ZV/mCXiBwQcK+LBbN8h+alOaIx9BD0= diff --git a/easyp.yaml b/easyp.yaml index 43ecb7d..21dbd96 100644 --- a/easyp.yaml +++ b/easyp.yaml @@ -1,19 +1,31 @@ -version: v1alpha lint: - use: - - DEFAULT + use: + - DEFAULT deps: - - github.com/googleapis/googleapis@common-protos-1_3_1 - - github.com/grpc-ecosystem/grpc-gateway + - github.com/googleapis/googleapis@common-protos-1_3_1 + - github.com/grpc-ecosystem/grpc-gateway@v2.19.1 generate: - inputs: - - directory: "api/v2" - plugins: - - name: go - out: gen/go - opt: - paths: source_relative - - name: go-grpc - out: gen/go - opt: - paths: source_relative + inputs: + - directory: "api/v2" + plugins: + - name: go + out: gen/go + opt: + paths: source_relative + module: "github.com/bmstu-itstech/scriptum-back" + with_imports: true + + - name: go-grpc + out: gen/go + opt: + paths: source_relative + + - name: grpc-gateway + out: gen/go + opts: + paths: source_relative + + - name: openapiv2 + out: ./gen/openapi + opts: + simple_operation_ids: true diff --git a/go.mod b/go.mod index 23434f4..0fb2d16 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/bmstu-itstech/scriptum-back -go 1.24.0 - -toolchain go1.24.3 +go 1.25.0 require ( + github.com/BOBAvov/protos_sso v0.0.17 github.com/ThreeDotsLabs/watermill v1.5.1 github.com/fatih/color v1.18.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 @@ -15,7 +14,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/zhikh23/pgutils v1.1.3 google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 - google.golang.org/grpc v1.74.2 + google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 ) @@ -52,11 +51,11 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 868a5a6..ca5fc2d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BOBAvov/protos_sso v0.0.17 h1:Dvhz4WnB8LNOKA5VE5RCCKWVqML1bdj6/Ck1f2vPKeU= +github.com/BOBAvov/protos_sso v0.0.17/go.mod h1:+bhsk/b46aqFYW3w8q01IxAiL914SpU5UicLhg/Nrx0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY= @@ -101,30 +103,32 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go index 4241688..23f567a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,7 @@ type Config struct { Logging Logging `mapstructure:"logging"` Postgres Postgres `mapstructure:"postgres"` Storage Storage `mapstructure:"storage"` + SSO SSO `mapstructure:"sso"` } type Docker struct { @@ -37,6 +38,12 @@ type Storage struct { BasePath string `mapstructure:"base_path"` } +type SSO struct { + Host string `mapstructure:"host"` + Port string `mapstructure:"port"` + AppID int32 `mapstructure:"app_id"` +} + func Load(path string) (*Config, error) { // Нетривиальный момент Viper, не описанный в документации, но описанный в // https://github.com/spf13/viper/issues/1797 diff --git a/internal/infra/mock/is_admin_provider.go b/internal/infra/mock/is_admin_provider.go deleted file mode 100644 index 5cd11a3..0000000 --- a/internal/infra/mock/is_admin_provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package mock - -import ( - "context" - - "github.com/bmstu-itstech/scriptum-back/internal/domain/value" -) - -type IsAdminProvider struct { - admins map[value.UserID]bool -} - -func NewIsAdminProvider() *IsAdminProvider { - return &IsAdminProvider{ - admins: make(map[value.UserID]bool), - } -} - -func (i *IsAdminProvider) IsAdmin(_ context.Context, uid value.UserID) (bool, error) { - return i.admins[uid], nil -} diff --git a/internal/infra/sso/is_admin_provider.go b/internal/infra/sso/is_admin_provider.go new file mode 100644 index 0000000..0423ae2 --- /dev/null +++ b/internal/infra/sso/is_admin_provider.go @@ -0,0 +1,75 @@ +package sso + +import ( + "context" + ssov1 "github.com/BOBAvov/protos_sso/gen/go/sso" + "github.com/bmstu-itstech/scriptum-back/internal/config" + "github.com/bmstu-itstech/scriptum-back/internal/domain/value" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "log/slog" + "net" + "strconv" +) + +type SSO struct { + conn *grpc.ClientConn + api ssov1.AuthClient + l *slog.Logger + AppID int32 +} + +func MustNewSSOClient(config config.SSO, l *slog.Logger) (*SSO, func() error) { + sso, closeFn, err := NewSSOClient(config, l) + if err != nil { + panic("Error creating SSO client: " + err.Error()) + } + return sso, closeFn +} + +func NewSSOClient(config config.SSO, l *slog.Logger) (*SSO, func() error, error) { + addr := net.JoinHostPort(config.Host, config.Port) + cc, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, func() error { return nil }, err + } + + closeFn := cc.Close + + return &SSO{ + api: ssov1.NewAuthClient(cc), + conn: cc, + l: l, + AppID: config.AppID, + }, closeFn, nil +} + +// IsAdmin checks if the user with the given uid has admin privileges. +// Note: uid parameter is of type value.UserID but is converted to int64 for the gRPC call. +func (s *SSO) IsAdmin(ctx context.Context, uid value.UserID) (bool, error) { + const op = "infra.SSO.IsAdmin" + + l := s.l.With( + slog.String("op", op), + slog.Int64("uid", int64(uid)), + ) + + l.Debug("Checking admin status") + // Add metadata with application ID to the context + ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs( + "x-user-id", strconv.FormatInt(int64(s.AppID), 10), + )) + // Call the IsAdmin method on the SSO server + resp, err := s.api.IsAdmin(ctx, &ssov1.IsAdminRequest{ + UserId: int64(uid), + }) + if err != nil { + l.Error("Failed to check admin status", slog.String("error", err.Error())) + return false, err + } + + l.Debug("Admin status", slog.Bool("is_admin", resp.IsAdmin)) + + return resp.IsAdmin, nil +} diff --git a/tests/suite/suite.go b/tests/suite/suite.go index 1432263..d3c2225 100644 --- a/tests/suite/suite.go +++ b/tests/suite/suite.go @@ -2,6 +2,7 @@ package suite import ( "context" + "fmt" "math/rand/v2" "net" "os" @@ -20,8 +21,8 @@ import ( "github.com/bmstu-itstech/scriptum-back/internal/config" "github.com/bmstu-itstech/scriptum-back/internal/infra/docker" "github.com/bmstu-itstech/scriptum-back/internal/infra/local" - "github.com/bmstu-itstech/scriptum-back/internal/infra/mock" "github.com/bmstu-itstech/scriptum-back/internal/infra/postgres" + "github.com/bmstu-itstech/scriptum-back/internal/infra/sso" "github.com/bmstu-itstech/scriptum-back/internal/infra/watermill" "github.com/bmstu-itstech/scriptum-back/pkg/logs" "github.com/bmstu-itstech/scriptum-back/pkg/server/auth" @@ -49,6 +50,11 @@ func suiteConfig() config.Config { Storage: config.Storage{ BasePath: "../uploads", }, + SSO: config.SSO{ + Host: os.Getenv("SSO_HOST"), + Port: os.Getenv("SSO_PORT"), + AppID: 1, + }, } } @@ -59,7 +65,13 @@ func New(t *testing.T) (context.Context, *Suite) { repos := postgres.MustNewRepository(cfg.Postgres, l) runner := docker.MustNewRunner(cfg.Docker, l) storage := local.MustNewStorage(cfg.Storage, l) - mockIAP := mock.NewIsAdminProvider() + ssoApi, closeFn := sso.MustNewSSOClient(cfg.SSO, l) + t.Cleanup(func() { + err := closeFn() + if err != nil { + l.Error(fmt.Sprintf("failed to close sso client: %s", err.Error())) + } + }) jPub, jSub := watermill.NewJobPubSubGoChannels(l) @@ -68,7 +80,7 @@ func New(t *testing.T) (context.Context, *Suite) { BoxRepo: repos, FileReader: storage, FileUploader: storage, - IsAdminProvider: mockIAP, + IsAdminProvider: ssoApi, JobProvider: repos, JobPublisher: jPub, JobRepository: repos,