diff --git a/pkg/roles/api/auth/api.go b/pkg/roles/api/auth/api.go index 9025b4089..566c0550d 100644 --- a/pkg/roles/api/auth/api.go +++ b/pkg/roles/api/auth/api.go @@ -30,9 +30,9 @@ func (ap *AuthProvider) APIConfig() usecase.Interactor { } type APIMeOutput struct { - Username string `json:"username" required:"true"` - Authenticated bool `json:"authenticated" required:"true"` - Permissions []Permission `json:"permissions" required:"true"` + Username string `json:"username" required:"true"` + Authenticated bool `json:"authenticated" required:"true"` + Permissions []*types.Permission `json:"permissions" required:"true"` } func (ap *AuthProvider) APIMe() usecase.Interactor { @@ -43,7 +43,7 @@ func (ap *AuthProvider) APIMe() usecase.Interactor { output.Authenticated = false return nil } - user := u.(User) + user := u.(*types.User) output.Authenticated = true output.Username = user.Username output.Permissions = user.Permissions diff --git a/pkg/roles/api/auth/api_tokens.go b/pkg/roles/api/auth/api_tokens.go index 3bf2c40e5..c211301f9 100644 --- a/pkg/roles/api/auth/api_tokens.go +++ b/pkg/roles/api/auth/api_tokens.go @@ -62,12 +62,11 @@ type APITokensPutOutput struct { func (ap *AuthProvider) APITokensPut() usecase.Interactor { u := usecase.NewInteractor(func(ctx context.Context, input APITokensPutInput, output *APITokensPutOutput) error { - token := &Token{ + token := &types.Token{ Key: base64.RawStdEncoding.EncodeToString(securecookie.GenerateRandomKey(64)), Username: input.Username, - ap: ap, } - err := token.put(ctx) + err := ap.putToken(token, ctx) if err != nil { return status.Wrap(err, status.Internal) } diff --git a/pkg/roles/api/auth/api_tokens_test.go b/pkg/roles/api/auth/api_tokens_test.go index 74c4aec22..ca73d6b57 100644 --- a/pkg/roles/api/auth/api_tokens_test.go +++ b/pkg/roles/api/auth/api_tokens_test.go @@ -20,14 +20,14 @@ func TestAPITokensGet(t *testing.T) { defer role.Stop() prov := auth.NewAuthProvider(role, inst) - tests.PanicIfError(inst.KV().Put( + tests.PanicIfError(inst.KV().PutObj( ctx, inst.KV().Key( types.KeyRole, types.KeyTokens, tests.RandomString(), ).String(), - tests.MustJSON(auth.Token{}), + &types.Token{}, )) var output auth.APITokensGetOutput @@ -58,7 +58,7 @@ func TestAPITokensPut(t *testing.T) { types.KeyTokens, output.Key, ), - auth.Token{ + &types.Token{ Username: name, }, ) @@ -75,14 +75,14 @@ func TestAPITokensDelete(t *testing.T) { name := tests.RandomString() - tests.PanicIfError(inst.KV().Put( + tests.PanicIfError(inst.KV().PutObj( ctx, inst.KV().Key( types.KeyRole, types.KeyTokens, name, ).String(), - tests.MustJSON(auth.Token{}), + &types.Token{}, )) assert.NoError(t, prov.APITokensDelete().Interact(ctx, auth.APITokensDeleteInput{ diff --git a/pkg/roles/api/auth/api_users.go b/pkg/roles/api/auth/api_users.go index cfa32949f..49025ef31 100644 --- a/pkg/roles/api/auth/api_users.go +++ b/pkg/roles/api/auth/api_users.go @@ -15,8 +15,8 @@ type APIUsersGetInput struct { Username string `query:"username" description:"Optional username of a user to get"` } type APIUser struct { - Username string `json:"username" required:"true"` - Permissions []Permission `json:"permissions" required:"true"` + Username string `json:"username" required:"true"` + Permissions []*types.Permission `json:"permissions" required:"true"` } type APIUsersGetOutput struct { Users []APIUser `json:"users" required:"true"` @@ -64,8 +64,8 @@ func (ap *AuthProvider) APIUsersGet() usecase.Interactor { type APIUsersPutInput struct { Username string `query:"username" required:"true"` - Password string `json:"password" required:"true"` - Permissions []Permission `json:"permissions" required:"true"` + Password string `json:"password" required:"true"` + Permissions []*types.Permission `json:"permissions" required:"true"` } func (ap *AuthProvider) APIUsersPut() usecase.Interactor { @@ -78,7 +78,7 @@ func (ap *AuthProvider) APIUsersPut() usecase.Interactor { input.Username, ).String(), ) - var oldUser *User + var oldUser *types.User if err == nil && len(rawUsers.Kvs) > 0 { user, err := ap.userFromKV(rawUsers.Kvs[0]) if err != nil { @@ -89,10 +89,9 @@ func (ap *AuthProvider) APIUsersPut() usecase.Interactor { oldUser = user } - user := &User{ + user := &types.User{ Username: input.Username, Permissions: input.Permissions, - ap: ap, } if input.Password != "" { hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) @@ -103,7 +102,7 @@ func (ap *AuthProvider) APIUsersPut() usecase.Interactor { } else if oldUser != nil { user.Password = oldUser.Password } - err = user.put(ctx) + err = ap.putUser(user, ctx) if err != nil { return status.Wrap(err, status.Internal) } diff --git a/pkg/roles/api/auth/api_users_test.go b/pkg/roles/api/auth/api_users_test.go index 83bc987a0..1c4ac547f 100644 --- a/pkg/roles/api/auth/api_users_test.go +++ b/pkg/roles/api/auth/api_users_test.go @@ -29,7 +29,7 @@ func TestAPIUsersGet(t *testing.T) { types.KeyUsers, tests.RandomString(), ).String(), - tests.MustJSON(auth.User{}), + string(tests.MustProto(&types.User{})), )) var output auth.APIUsersGetOutput @@ -92,7 +92,7 @@ func TestAPIUsersDelete(t *testing.T) { types.KeyUsers, name, ).String(), - tests.MustJSON(auth.User{}), + string(tests.MustProto(&types.User{})), )) assert.NoError(t, prov.APIUsersDelete().Interact(ctx, auth.APIUsersDeleteInput{ diff --git a/pkg/roles/api/auth/first_start.go b/pkg/roles/api/auth/first_start.go index 6584122aa..27c30e06b 100644 --- a/pkg/roles/api/auth/first_start.go +++ b/pkg/roles/api/auth/first_start.go @@ -7,6 +7,7 @@ import ( "beryju.io/gravity/pkg/extconfig" "beryju.io/gravity/pkg/roles" + "beryju.io/gravity/pkg/roles/api/types" "github.com/gorilla/securecookie" "go.uber.org/zap" ) @@ -36,12 +37,11 @@ func (ap *AuthProvider) FirstStart(ev *roles.Event) { token := os.Getenv("ADMIN_TOKEN") if token != "" { - t := Token{ + t := &types.Token{ Key: token, Username: username, - ap: ap, } - err := t.put(ev.Context) + err := ap.putToken(t, ev.Context) if err != nil { ap.log.Warn("failed to create bootstrap token", zap.Error(err)) return diff --git a/pkg/roles/api/auth/method_api_key.go b/pkg/roles/api/auth/method_api_key.go index 237d60646..d88e104f9 100644 --- a/pkg/roles/api/auth/method_api_key.go +++ b/pkg/roles/api/auth/method_api_key.go @@ -62,6 +62,6 @@ func (ap *AuthProvider) checkStaticToken(r *http.Request) bool { return false } session := r.Context().Value(types.RequestSession).(*sessions.Session) - session.Values[types.SessionKeyUser] = *user + session.Values[types.SessionKeyUser] = user return false } diff --git a/pkg/roles/api/auth/method_oidc.go b/pkg/roles/api/auth/method_oidc.go index 7a46eb670..bcbf9a675 100644 --- a/pkg/roles/api/auth/method_oidc.go +++ b/pkg/roles/api/auth/method_oidc.go @@ -92,10 +92,10 @@ func (ap *AuthProvider) oidcCallback(w http.ResponseWriter, r *http.Request) { http.Error(w, "failed to authenticate", http.StatusBadRequest) return } - user := User{ + user := &types.User{ Username: claims.Email, Password: "", - Permissions: []Permission{ + Permissions: []*types.Permission{ { Path: "/*", Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, @@ -150,6 +150,6 @@ func (ap *AuthProvider) checkJWTToken(r *http.Request) bool { return false } session := r.Context().Value(types.RequestSession).(*sessions.Session) - session.Values[types.SessionKeyUser] = *user + session.Values[types.SessionKeyUser] = user return false } diff --git a/pkg/roles/api/auth/method_oidc_test.go b/pkg/roles/api/auth/method_oidc_test.go index 8386510e3..66384c36c 100644 --- a/pkg/roles/api/auth/method_oidc_test.go +++ b/pkg/roles/api/auth/method_oidc_test.go @@ -87,7 +87,7 @@ func TestAuthOIDC_Token(t *testing.T) { types.KeyUsers, "admin@example.com", ).String(), - tests.MustJSON(auth.User{}), + string(tests.MustProto(&types.User{})), )) role := api.New(inst) diff --git a/pkg/roles/api/auth/middleware.go b/pkg/roles/api/auth/middleware.go index afc02db72..a1486ddfe 100644 --- a/pkg/roles/api/auth/middleware.go +++ b/pkg/roles/api/auth/middleware.go @@ -34,10 +34,11 @@ func (ap *AuthProvider) isRequestAllowed(r *http.Request) bool { if hub == nil { hub = sentry.CurrentHub() } + uu := u.(*types.User) hub.Scope().SetUser(sentry.User{ - Username: u.(User).Username, + Username: uu.Username, }) - return ap.checkPermission(r, u.(User)) + return ap.checkPermission(r, uu) } func (ap *AuthProvider) ServeHTTP(rw http.ResponseWriter, r *http.Request) { diff --git a/pkg/roles/api/auth/permission.go b/pkg/roles/api/auth/permission.go index 06cb9fac9..a8067acfd 100644 --- a/pkg/roles/api/auth/permission.go +++ b/pkg/roles/api/auth/permission.go @@ -3,20 +3,22 @@ package auth import ( "net/http" "strings" + + "beryju.io/gravity/pkg/roles/api/types" ) const wildcard = "*" -func (ap *AuthProvider) checkPermission(req *http.Request, u User) bool { - var longestMatch *Permission +func (ap *AuthProvider) checkPermission(req *http.Request, u *types.User) bool { + var longestMatch *types.Permission for _, perm := range u.Permissions { if strings.HasSuffix(perm.Path, wildcard) && strings.HasPrefix(req.URL.Path, strings.TrimSuffix(perm.Path, wildcard)) { if longestMatch == nil || len(perm.Path) > len(longestMatch.Path) { - longestMatch = &perm + longestMatch = perm } } else if perm.Path == req.URL.Path { if longestMatch == nil || len(perm.Path) > len(longestMatch.Path) { - longestMatch = &perm + longestMatch = perm } } } diff --git a/pkg/roles/api/auth/permissions_test.go b/pkg/roles/api/auth/permissions_test.go index 36991eade..da72cec14 100644 --- a/pkg/roles/api/auth/permissions_test.go +++ b/pkg/roles/api/auth/permissions_test.go @@ -4,6 +4,7 @@ import ( "net/http" "testing" + "beryju.io/gravity/pkg/roles/api/types" "github.com/stretchr/testify/assert" ) @@ -17,8 +18,8 @@ func mustRequest(meth string, url string) *http.Request { func TestPermission_Fixed(t *testing.T) { ap := AuthProvider{} - assert.True(t, ap.checkPermission(mustRequest("get", "/foo/bar"), User{ - Permissions: []Permission{ + assert.True(t, ap.checkPermission(mustRequest("get", "/foo/bar"), &types.User{ + Permissions: []*types.Permission{ { Path: "/foo/bar", Methods: []string{"get", "post"}, @@ -37,16 +38,16 @@ func TestPermission_Fixed(t *testing.T) { func TestPermission_Wildcard(t *testing.T) { ap := AuthProvider{} - assert.True(t, ap.checkPermission(mustRequest("get", "/foo/bar"), User{ - Permissions: []Permission{ + assert.True(t, ap.checkPermission(mustRequest("get", "/foo/bar"), &types.User{ + Permissions: []*types.Permission{ { Path: "/foo/*", Methods: []string{"get"}, }, }, })) - assert.True(t, ap.checkPermission(mustRequest("get", "/foo/bar"), User{ - Permissions: []Permission{ + assert.True(t, ap.checkPermission(mustRequest("get", "/foo/bar"), &types.User{ + Permissions: []*types.Permission{ { Path: "/foo/*", Methods: []string{"*"}, diff --git a/pkg/roles/api/auth/provider.go b/pkg/roles/api/auth/provider.go index bfd0967c3..e86cbd593 100644 --- a/pkg/roles/api/auth/provider.go +++ b/pkg/roles/api/auth/provider.go @@ -3,7 +3,6 @@ package auth import ( "context" "encoding/gob" - "encoding/json" "net/http" instanceTypes "beryju.io/gravity/pkg/instance/types" @@ -40,7 +39,7 @@ func NewAuthProvider(r roles.Role, inst roles.Instance) *AuthProvider { "/api/v1/openapi.json", }, } - gob.Register(User{}) + gob.Register(types.User{}) inst.AddEventListener(types.EventTopicAPIMuxSetup, func(ev *roles.Event) { svc := ev.Payload.Data["svc"].(*web.Service) mux := ev.Payload.Data["mux"].(*mux.Router) @@ -70,28 +69,24 @@ func (ap *AuthProvider) CreateUser(ctx context.Context, username, password strin return err } - user := User{ + user := types.User{ Password: string(hashedPw), - Permissions: []Permission{ + Permissions: []*types.Permission{ { Path: "/*", Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, }, }, } - userJson, err := json.Marshal(user) - if err != nil { - return err - } - _, err = ap.inst.KV().Put( + _, err = ap.inst.KV().PutObj( ctx, ap.inst.KV().Key( types.KeyRole, types.KeyUsers, username, ).String(), - string(userJson), + &user, ) if err != nil { return err diff --git a/pkg/roles/api/auth/token.go b/pkg/roles/api/auth/token.go index afcd5d3a6..5c9c3fd1d 100644 --- a/pkg/roles/api/auth/token.go +++ b/pkg/roles/api/auth/token.go @@ -2,7 +2,6 @@ package auth import ( "context" - "encoding/json" "strings" "beryju.io/gravity/pkg/roles/api/types" @@ -15,39 +14,26 @@ const ( BearerType = "bearer" ) -type Token struct { - ap *AuthProvider - Key string `json:"-"` - - Username string `json:"username"` -} - -func (token *Token) put(ctx context.Context, opts ...clientv3.OpOption) error { - raw, err := json.Marshal(&token) - if err != nil { - return err - } - fullKey := token.ap.inst.KV().Key( +func (ap *AuthProvider) putToken(t *types.Token, ctx context.Context, opts ...clientv3.OpOption) error { + fullKey := ap.inst.KV().Key( types.KeyRole, types.KeyTokens, - token.Key, + t.Key, ).String() - _, err = token.ap.inst.KV().Put(ctx, fullKey, string(raw), opts...) + _, err := ap.inst.KV().PutObj(ctx, fullKey, t, opts...) return err } -func (ap *AuthProvider) tokenFromKV(raw *mvccpb.KeyValue) (*Token, error) { - token := &Token{ - ap: ap, - } +func (ap *AuthProvider) tokenFromKV(raw *mvccpb.KeyValue) (*types.Token, error) { + token := &types.Token{} prefix := ap.inst.KV().Key( types.KeyRole, types.KeyTokens, ).Prefix(true).String() - token.Key = strings.TrimPrefix(string(raw.Key), prefix) - err := json.Unmarshal(raw.Value, &token) + err := ap.inst.KV().Unmarshal(raw.Value, &token) if err != nil { return token, err } + token.Key = strings.TrimPrefix(string(raw.Key), prefix) return token, nil } diff --git a/pkg/roles/api/auth/user.go b/pkg/roles/api/auth/user.go index b8f82ecbc..dbf531750 100644 --- a/pkg/roles/api/auth/user.go +++ b/pkg/roles/api/auth/user.go @@ -2,7 +2,6 @@ package auth import ( "context" - "encoding/json" "strings" "beryju.io/gravity/pkg/roles/api/types" @@ -10,49 +9,26 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" ) -type Permission struct { - Path string `json:"path"` - Methods []string `json:"methods"` -} - -type User struct { - ap *AuthProvider - - Username string `json:"-"` - Password string `json:"password"` - Permissions []Permission `json:"permissions"` -} - -func (u *User) String() string { - return u.Username -} - -func (ap *AuthProvider) userFromKV(raw *mvccpb.KeyValue) (*User, error) { - user := &User{ - ap: ap, - } +func (ap *AuthProvider) userFromKV(raw *mvccpb.KeyValue) (*types.User, error) { + user := &types.User{} prefix := ap.inst.KV().Key( types.KeyRole, types.KeyUsers, ).Prefix(true).String() - user.Username = strings.TrimPrefix(string(raw.Key), prefix) - err := json.Unmarshal(raw.Value, &user) + err := ap.inst.KV().Unmarshal(raw.Value, user) if err != nil { return user, err } + user.Username = strings.TrimPrefix(string(raw.Key), prefix) return user, nil } -func (u *User) put(ctx context.Context, opts ...clientv3.OpOption) error { - raw, err := json.Marshal(&u) - if err != nil { - return err - } - fullKey := u.ap.inst.KV().Key( +func (ap *AuthProvider) putUser(u *types.User, ctx context.Context, opts ...clientv3.OpOption) error { + fullKey := ap.inst.KV().Key( types.KeyRole, types.KeyUsers, u.Username, ).String() - _, err = u.ap.inst.KV().Put(ctx, fullKey, string(raw), opts...) + _, err := ap.inst.KV().PutObj(ctx, fullKey, u, opts...) return err } diff --git a/pkg/roles/api/middleware/middleware_log.go b/pkg/roles/api/middleware/middleware_log.go index 5695fddab..21f83e10d 100644 --- a/pkg/roles/api/middleware/middleware_log.go +++ b/pkg/roles/api/middleware/middleware_log.go @@ -7,7 +7,6 @@ import ( "net/http" "time" - "beryju.io/gravity/pkg/roles/api/auth" "beryju.io/gravity/pkg/roles/api/types" "github.com/gorilla/sessions" "go.uber.org/zap" @@ -113,7 +112,7 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { session := se.(*sessions.Session) u, ok := session.Values[types.SessionKeyUser] if ok && u != nil { - if uu, castOk := u.(auth.User); castOk { + if uu, castOk := u.(*types.User); castOk { fields = append(fields, zap.String("user", uu.Username)) } } diff --git a/pkg/roles/api/role.go b/pkg/roles/api/role.go index 39508e0a2..300abb836 100644 --- a/pkg/roles/api/role.go +++ b/pkg/roles/api/role.go @@ -185,9 +185,9 @@ func (r *Role) ListenAndServeSocket() { r.socketServer.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx := context.WithValue(req.Context(), types.RequestSession, &sessions.Session{ Values: map[interface{}]interface{}{ - types.SessionKeyUser: auth.User{ + types.SessionKeyUser: &types.User{ Username: "gravity-socket", - Permissions: []auth.Permission{ + Permissions: []*types.Permission{ { Path: "/*", Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, diff --git a/pkg/roles/api/role_migrations.go b/pkg/roles/api/role_migrations.go index 77e91011e..8f1bbfe3b 100644 --- a/pkg/roles/api/role_migrations.go +++ b/pkg/roles/api/role_migrations.go @@ -2,12 +2,10 @@ package api import ( "context" - "encoding/json" "net/http" "strings" "beryju.io/gravity/pkg/instance/migrate" - "beryju.io/gravity/pkg/roles/api/auth" "beryju.io/gravity/pkg/roles/api/types" "beryju.io/gravity/pkg/storage" "github.com/Masterminds/semver/v3" @@ -20,7 +18,7 @@ func (r *Role) RegisterMigrations() { ActivateFunc: func(v *semver.Version) bool { return true }, HookFunc: func(ctx context.Context) (*storage.Client, error) { userPrefix := r.i.KV().Key(types.KeyRole, types.KeyUsers).Prefix(true).String() - defaultPerms := []auth.Permission{ + defaultPerms := []types.Permission{ { Path: "/*", Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, @@ -31,15 +29,15 @@ func (r *Role) RegisterMigrations() { shouldIntercept := res != nil && len(res.Kvs) > 0 && strings.HasPrefix(key, userPrefix) // If we're fetching a user, intercept the response if shouldIntercept { - u := map[string]interface{}{} - err := json.Unmarshal(res.Kvs[0].Value, &u) + u := map[string]any{} + err := r.i.KV().Unmarshal(res.Kvs[0].Value, &u) if err != nil { return res, nil } if _, set := u["permissions"]; !set { u["permissions"] = defaultPerms } - v, err := json.Marshal(u) + v, err := r.i.KV().Marshal(u) if err != nil { return res, nil } diff --git a/pkg/roles/api/types/role_api_token.pb.go b/pkg/roles/api/types/role_api_token.pb.go new file mode 100644 index 000000000..a9e72b523 --- /dev/null +++ b/pkg/roles/api/types/role_api_token.pb.go @@ -0,0 +1,131 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc v6.32.1 +// source: protobuf/role_api_token.proto + +package types + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Token struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Token) Reset() { + *x = Token{} + mi := &file_protobuf_role_api_token_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Token) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Token) ProtoMessage() {} + +func (x *Token) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_role_api_token_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Token.ProtoReflect.Descriptor instead. +func (*Token) Descriptor() ([]byte, []int) { + return file_protobuf_role_api_token_proto_rawDescGZIP(), []int{0} +} + +func (x *Token) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Token) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +var File_protobuf_role_api_token_proto protoreflect.FileDescriptor + +const file_protobuf_role_api_token_proto_rawDesc = "" + + "\n" + + "\x1dprotobuf/role_api_token.proto\"5\n" + + "\x05Token\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busernameB\x15Z\x13pkg/roles/api/typesb\x06proto3" + +var ( + file_protobuf_role_api_token_proto_rawDescOnce sync.Once + file_protobuf_role_api_token_proto_rawDescData []byte +) + +func file_protobuf_role_api_token_proto_rawDescGZIP() []byte { + file_protobuf_role_api_token_proto_rawDescOnce.Do(func() { + file_protobuf_role_api_token_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protobuf_role_api_token_proto_rawDesc), len(file_protobuf_role_api_token_proto_rawDesc))) + }) + return file_protobuf_role_api_token_proto_rawDescData +} + +var file_protobuf_role_api_token_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_protobuf_role_api_token_proto_goTypes = []any{ + (*Token)(nil), // 0: Token +} +var file_protobuf_role_api_token_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_protobuf_role_api_token_proto_init() } +func file_protobuf_role_api_token_proto_init() { + if File_protobuf_role_api_token_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_protobuf_role_api_token_proto_rawDesc), len(file_protobuf_role_api_token_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_protobuf_role_api_token_proto_goTypes, + DependencyIndexes: file_protobuf_role_api_token_proto_depIdxs, + MessageInfos: file_protobuf_role_api_token_proto_msgTypes, + }.Build() + File_protobuf_role_api_token_proto = out.File + file_protobuf_role_api_token_proto_goTypes = nil + file_protobuf_role_api_token_proto_depIdxs = nil +} diff --git a/pkg/roles/api/types/role_api_user.pb.go b/pkg/roles/api/types/role_api_user.pb.go new file mode 100644 index 000000000..735de3efa --- /dev/null +++ b/pkg/roles/api/types/role_api_user.pb.go @@ -0,0 +1,198 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc v6.32.1 +// source: protobuf/role_api_user.proto + +package types + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Permission struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Methods []string `protobuf:"bytes,2,rep,name=methods,proto3" json:"methods,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Permission) Reset() { + *x = Permission{} + mi := &file_protobuf_role_api_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Permission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Permission) ProtoMessage() {} + +func (x *Permission) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_role_api_user_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Permission.ProtoReflect.Descriptor instead. +func (*Permission) Descriptor() ([]byte, []int) { + return file_protobuf_role_api_user_proto_rawDescGZIP(), []int{0} +} + +func (x *Permission) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Permission) GetMethods() []string { + if x != nil { + return x.Methods + } + return nil +} + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + Permissions []*Permission `protobuf:"bytes,3,rep,name=permissions,proto3" json:"permissions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_protobuf_role_api_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_role_api_user_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_protobuf_role_api_user_proto_rawDescGZIP(), []int{1} +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *User) GetPermissions() []*Permission { + if x != nil { + return x.Permissions + } + return nil +} + +var File_protobuf_role_api_user_proto protoreflect.FileDescriptor + +const file_protobuf_role_api_user_proto_rawDesc = "" + + "\n" + + "\x1cprotobuf/role_api_user.proto\":\n" + + "\n" + + "Permission\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x18\n" + + "\amethods\x18\x02 \x03(\tR\amethods\"m\n" + + "\x04User\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\x12-\n" + + "\vpermissions\x18\x03 \x03(\v2\v.PermissionR\vpermissionsB\x15Z\x13pkg/roles/api/typesb\x06proto3" + +var ( + file_protobuf_role_api_user_proto_rawDescOnce sync.Once + file_protobuf_role_api_user_proto_rawDescData []byte +) + +func file_protobuf_role_api_user_proto_rawDescGZIP() []byte { + file_protobuf_role_api_user_proto_rawDescOnce.Do(func() { + file_protobuf_role_api_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protobuf_role_api_user_proto_rawDesc), len(file_protobuf_role_api_user_proto_rawDesc))) + }) + return file_protobuf_role_api_user_proto_rawDescData +} + +var file_protobuf_role_api_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_protobuf_role_api_user_proto_goTypes = []any{ + (*Permission)(nil), // 0: Permission + (*User)(nil), // 1: User +} +var file_protobuf_role_api_user_proto_depIdxs = []int32{ + 0, // 0: User.permissions:type_name -> Permission + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_protobuf_role_api_user_proto_init() } +func file_protobuf_role_api_user_proto_init() { + if File_protobuf_role_api_user_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_protobuf_role_api_user_proto_rawDesc), len(file_protobuf_role_api_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_protobuf_role_api_user_proto_goTypes, + DependencyIndexes: file_protobuf_role_api_user_proto_depIdxs, + MessageInfos: file_protobuf_role_api_user_proto_msgTypes, + }.Build() + File_protobuf_role_api_user_proto = out.File + file_protobuf_role_api_user_proto_goTypes = nil + file_protobuf_role_api_user_proto_depIdxs = nil +} diff --git a/pkg/roles/tsdb/api.go b/pkg/roles/tsdb/api.go index cfaa160bd..4b5720294 100644 --- a/pkg/roles/tsdb/api.go +++ b/pkg/roles/tsdb/api.go @@ -13,7 +13,6 @@ import ( "github.com/swaggest/usecase/status" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" - "google.golang.org/protobuf/proto" ) func (r *Role) APIMetrics() usecase.Interactor { @@ -52,7 +51,7 @@ func (r *Role) APIMetrics() usecase.Interactor { continue } v := types.MetricsRecord{} - err = proto.Unmarshal(kv.Value, &v) + err = r.i.KV().Unmarshal(kv.Value, &v) if err != nil { value, err := strconv.ParseInt(string(kv.Value), 10, 0) if err != nil { diff --git a/pkg/roles/tsdb/role.go b/pkg/roles/tsdb/role.go index b2638f777..5a997d996 100644 --- a/pkg/roles/tsdb/role.go +++ b/pkg/roles/tsdb/role.go @@ -20,7 +20,6 @@ import ( "github.com/swaggest/rest/web" clientv3 "go.etcd.io/etcd/client/v3" "go.uber.org/zap" - "google.golang.org/protobuf/proto" ) type Role struct { @@ -170,14 +169,10 @@ func (r *Role) write(ctx context.Context) { v := types.MetricsRecord{ Value: int64(value.Value), } - rv, err := proto.Marshal(&v) - if err != nil { - r.log.Warn("failed to marshal message", zap.Error(err)) - } - _, err = r.i.KV().Put( + _, err = r.i.KV().PutObj( ks.Context(), key, - string(rv), + &v, clientv3.WithLease(lease.ID), ) if err != nil { diff --git a/pkg/roles/tsdb/types/role_tsdb_record.pb.go b/pkg/roles/tsdb/types/role_tsdb_record.pb.go index 66365860c..f45b91f54 100644 --- a/pkg/roles/tsdb/types/role_tsdb_record.pb.go +++ b/pkg/roles/tsdb/types/role_tsdb_record.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.28.3 +// protoc-gen-go v1.36.8 +// protoc v6.32.1 // source: protobuf/role_tsdb_record.proto package types @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,20 +22,17 @@ const ( ) type MetricsRecord struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *MetricsRecord) Reset() { *x = MetricsRecord{} - if protoimpl.UnsafeEnabled { - mi := &file_protobuf_role_tsdb_record_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_protobuf_role_tsdb_record_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *MetricsRecord) String() string { @@ -45,7 +43,7 @@ func (*MetricsRecord) ProtoMessage() {} func (x *MetricsRecord) ProtoReflect() protoreflect.Message { mi := &file_protobuf_role_tsdb_record_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -69,24 +67,20 @@ func (x *MetricsRecord) GetValue() int64 { var File_protobuf_role_tsdb_record_proto protoreflect.FileDescriptor -var file_protobuf_role_tsdb_record_proto_rawDesc = []byte{ - 0x0a, 0x1f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, - 0x74, 0x73, 0x64, 0x62, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x25, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x16, 0x5a, 0x14, 0x70, 0x6b, 0x67, 0x2f, - 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x2f, 0x74, 0x73, 0x64, 0x62, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_protobuf_role_tsdb_record_proto_rawDesc = "" + + "\n" + + "\x1fprotobuf/role_tsdb_record.proto\"%\n" + + "\rMetricsRecord\x12\x14\n" + + "\x05value\x18\x01 \x01(\x03R\x05valueB\x16Z\x14pkg/roles/tsdb/typesb\x06proto3" var ( file_protobuf_role_tsdb_record_proto_rawDescOnce sync.Once - file_protobuf_role_tsdb_record_proto_rawDescData = file_protobuf_role_tsdb_record_proto_rawDesc + file_protobuf_role_tsdb_record_proto_rawDescData []byte ) func file_protobuf_role_tsdb_record_proto_rawDescGZIP() []byte { file_protobuf_role_tsdb_record_proto_rawDescOnce.Do(func() { - file_protobuf_role_tsdb_record_proto_rawDescData = protoimpl.X.CompressGZIP(file_protobuf_role_tsdb_record_proto_rawDescData) + file_protobuf_role_tsdb_record_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protobuf_role_tsdb_record_proto_rawDesc), len(file_protobuf_role_tsdb_record_proto_rawDesc))) }) return file_protobuf_role_tsdb_record_proto_rawDescData } @@ -108,25 +102,11 @@ func file_protobuf_role_tsdb_record_proto_init() { if File_protobuf_role_tsdb_record_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_protobuf_role_tsdb_record_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*MetricsRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_protobuf_role_tsdb_record_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_protobuf_role_tsdb_record_proto_rawDesc), len(file_protobuf_role_tsdb_record_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, @@ -137,7 +117,6 @@ func file_protobuf_role_tsdb_record_proto_init() { MessageInfos: file_protobuf_role_tsdb_record_proto_msgTypes, }.Build() File_protobuf_role_tsdb_record_proto = out.File - file_protobuf_role_tsdb_record_proto_rawDesc = nil file_protobuf_role_tsdb_record_proto_goTypes = nil file_protobuf_role_tsdb_record_proto_depIdxs = nil } diff --git a/pkg/storage/client.go b/pkg/storage/client.go index e4faabc20..557bf9931 100644 --- a/pkg/storage/client.go +++ b/pkg/storage/client.go @@ -2,10 +2,12 @@ package storage import ( "context" + "encoding/json" "time" "beryju.io/gravity/pkg/storage/trace" "go.uber.org/zap" + "google.golang.org/protobuf/proto" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/namespace" @@ -157,3 +159,28 @@ func (c *Client) Delete(ctx context.Context, key string, opts ...clientv3.OpOpti } return res, err } + +func (c *Client) PutObj(ctx context.Context, key string, val any, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { + marshalled, err := c.Marshal(val) + if err != nil { + return nil, err + } + return c.Put(ctx, key, string(marshalled), opts...) +} + +func (c *Client) Unmarshal(raw []byte, out any) error { + err := json.Unmarshal(raw, out) + if err != nil { + if p, ok := out.(proto.Message); ok { + return proto.Unmarshal(raw, p) + } + } + return err +} + +func (c *Client) Marshal(in any) ([]byte, error) { + if p, ok := in.(proto.Message); ok { + return proto.Marshal(p) + } + return json.Marshal(in) +} diff --git a/pkg/tests/api.go b/pkg/tests/api.go index 584b13021..5f3ec883e 100644 --- a/pkg/tests/api.go +++ b/pkg/tests/api.go @@ -29,9 +29,9 @@ func APIClient(rootInst *instance.Instance) (*api.APIClient, func()) { types.KeyRole, types.KeyUsers, username, - ).String(), MustJSON(auth.User{ + ).String(), MustJSON(&types.User{ Username: username, - Permissions: []auth.Permission{ + Permissions: []*types.Permission{ { Path: "/*", Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodHead, http.MethodDelete}, diff --git a/protobuf/role_api_token.proto b/protobuf/role_api_token.proto new file mode 100644 index 000000000..f404bc077 --- /dev/null +++ b/protobuf/role_api_token.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option go_package = "pkg/roles/api/types"; + +message Token { + string key = 1; + string username = 2; +} diff --git a/protobuf/role_api_user.proto b/protobuf/role_api_user.proto new file mode 100644 index 000000000..a3db7bf95 --- /dev/null +++ b/protobuf/role_api_user.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +option go_package = "pkg/roles/api/types"; + +message Permission { + string path = 1; + repeated string methods = 2; +} + +message User { + string username = 1; + string password = 2; + repeated Permission permissions = 3; +}