Changes
3 changed files (+203/-118)
-
-
@@ -0,0 +1,180 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package workspace import ( "bytes" "context" "crypto/rand" "strings" "connectrpc.com/connect" "github.com/google/uuid" "google.golang.org/protobuf/proto" eventV1 "pocka.jp/x/yamori/proto/go/backend/events/v1" errorV1 "pocka.jp/x/yamori/proto/go/error/v1" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" "pocka.jp/x/yamori/backend/core/event" "pocka.jp/x/yamori/backend/core/projection" "pocka.jp/x/yamori/backend/crypto" workspaceEvent "pocka.jp/x/yamori/backend/events/workspace" ) func (s *Service) CreateInitialAdmin( ctx context.Context, req *connect.Request[workspaceV2.CreateInitialAdminRequest], ) (*connect.Response[workspaceV2.CreateInitialAdminResponse], error) { initialAdminPassword := req.Msg.GetInitialAdminPassword() if initialAdminPassword == "" { return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_MissingFieldError{ MissingFieldError: &errorV1.MissingFieldError{ Path: proto.String("initial_admin_password"), }, }, }), nil } name := req.Msg.GetName() if name == "" { return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_MissingFieldError{ MissingFieldError: &errorV1.MissingFieldError{ Path: proto.String("name"), }, }, }), nil } // TODO: SystemError 以外にする if strings.Trim(name, " \r\n\t") != name { return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Name cannot have heading or trailing spaces"), }, }, }), nil } displayName := strings.Trim(req.Msg.GetDisplayName(), " \r\n\t") if displayName == "" { displayName = name } password := req.Msg.GetPassword() if password == "" { return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_MissingFieldError{ MissingFieldError: &errorV1.MissingFieldError{ Path: proto.String("password"), }, }, }), nil } // TODO: SystemError 以外にする if len(password) <= 8 { return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Password has to be longer than or equals to 8 bytes"), }, }, }), nil } tx, err := s.core.DB.Begin() if err != nil { return nil, err } defer tx.Rollback() ws, err := projection.GetWorkspace(tx) if err != nil { return nil, err } pw, err := projection.GetAdminCreationPassword(tx) if err != nil { return nil, err } users, err := projection.GetUsers(tx) if err != nil { return nil, err } if err := event.UpdateProjections(tx, ws, pw, users); err != nil { return nil, err } if pw.Projection.Password == nil { tx.Commit() return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_PasswordExpired{ PasswordExpired: &errorV1.AuthenticationError{}, }, }), nil } hash := crypto.HashPassword([]byte(initialAdminPassword), pw.Projection.Password.Salt) if !bytes.Equal(hash, pw.Projection.Password.Hash) { return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }), nil } for _, u := range users.Projection.Users { if u.GetName() == name { // TODO: SystemError 以外にする return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("User with same name already exists."), }, }, }), nil } } id, err := uuid.NewRandom() if err != nil { return nil, err } keyID := make([]byte, 32) rand.Read(keyID) err = event.AppendEvents(tx, []*eventV1.Event{ workspaceEvent.ExpireAdminCreationPassword(), workspaceEvent.CreateUser(id.String(), name, displayName, keyID), workspaceEvent.GrantAdminAccess(id.String()), workspaceEvent.ConfigurePasswordLogin(id.String(), password), }) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return connect.NewResponse(&workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_Ok{ Ok: &workspaceV2.User{ Id: &workspaceV2.UserID{ Value: proto.String(id.String()), }, Name: proto.String(name), DisplayName: proto.String(displayName), IsAdmin: proto.Bool(true), }, }, }), nil }
-
-
-
@@ -6,20 +6,16 @@import ( "bytes" "context" "crypto/rand" "net/http" "strings" "connectrpc.com/connect" "github.com/google/uuid" "google.golang.org/protobuf/proto" "pocka.jp/x/yamori/backend/core" "pocka.jp/x/yamori/backend/core/event" "pocka.jp/x/yamori/backend/core/projection" "pocka.jp/x/yamori/backend/crypto" workspaceEvent "pocka.jp/x/yamori/backend/events/workspace" eventV1 "pocka.jp/x/yamori/proto/go/backend/events/v1" errorV1 "pocka.jp/x/yamori/proto/go/error/v1" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" workspaceV2connect "pocka.jp/x/yamori/proto/go/workspace/v2/v2connect"
-
@@ -219,99 +215,8 @@ },}), nil } // TODO: チェックが煩雑なので CreateUser と別のエンドポイントにする initialAdminPassword := req.Msg.GetInitialAdminPassword() if initialAdminPassword != "" { tx, err := s.core.DB.Begin() if err != nil { return nil, err } defer tx.Rollback() ws, err := projection.GetWorkspace(tx) if err != nil { return nil, err } pw, err := projection.GetAdminCreationPassword(tx) if err != nil { return nil, err } if err := event.UpdateProjections(tx, ws, pw); err != nil { return nil, err } if pw.Projection.Password == nil { tx.Commit() return connect.NewResponse(&workspaceV2.CreateUserResponse{ Result: &workspaceV2.CreateUserResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }), nil } hash := crypto.HashPassword([]byte(initialAdminPassword), pw.Projection.Password.Salt) if !bytes.Equal(hash, pw.Projection.Password.Hash) { return connect.NewResponse(&workspaceV2.CreateUserResponse{ Result: &workspaceV2.CreateUserResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }), nil } id, err := uuid.NewRandom() if err != nil { return nil, err } keyID := make([]byte, 32) rand.Read(keyID) err = event.AppendEvents(tx, []*eventV1.Event{ workspaceEvent.ExpireAdminCreationPassword(), workspaceEvent.CreateUser(id.String(), name, displayName, keyID), workspaceEvent.GrantAdminAccess(id.String()), workspaceEvent.ConfigurePasswordLogin(id.String(), password), }) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return connect.NewResponse(&workspaceV2.CreateUserResponse{ Result: &workspaceV2.CreateUserResponse_Ok{ Ok: &workspaceV2.User{ Id: &workspaceV2.UserID{ Value: proto.String(id.String()), }, Name: proto.String(name), DisplayName: proto.String(displayName), }, }, }), nil } res := workspaceV2.CreateUserResponse{ Result: &workspaceV2.CreateUserResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), }, }, } return connect.NewResponse(&res), nil } func (s *Service) CreateInitialAdmin( ctx context.Context, req *connect.Request[workspaceV2.CreateInitialAdminRequest], ) (*connect.Response[workspaceV2.CreateInitialAdminResponse], error) { res := workspaceV2.CreateInitialAdminResponse{ Result: &workspaceV2.CreateInitialAdminResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), },
-
-
packages/backend/tests/workspace/v2/create_user_test.go > packages/backend/tests/workspace/v2/create_initial_admin_test.go
-
@@ -58,9 +58,9 @@ server.Client(),server.URL(), ) res, err := client.CreateUser( res, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ Name: proto.String("alice"), DisplayName: proto.String("Alice"), Password: proto.String("Alice's password"),
-
@@ -71,7 +71,7 @@ if err != nil {t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.CreateUserResponse_Ok) v, ok := res.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Errorf("Expected ok, got %s", typeName.Type().Name())
-
@@ -108,7 +108,7 @@ }if login.Ok.Id.GetValue() != v.Ok.Id.GetValue() { t.Errorf( "CreateUserResponse.ok.id.value = %s, LoginResponse.ok.id.value = %s", "CreateInitialAdminResponse.ok.id.value = %s, LoginResponse.ok.id.value = %s", v.Ok.Id.GetValue(), login.Ok.Id.GetValue(), )
-
@@ -123,9 +123,9 @@ server.Client(),server.URL(), ) res, err := client.CreateUser( res, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ Name: proto.String("alice"), Password: proto.String("Alice's password"), InitialAdminPassword: proto.String("initial_admin_password"),
-
@@ -135,7 +135,7 @@ if err != nil {t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.CreateUserResponse_Ok) v, ok := res.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Errorf("Expected ok, got %s", typeName.Type().Name())
-
@@ -154,9 +154,9 @@ server.Client(),server.URL(), ) aliceRes, err := client.CreateUser( aliceRes, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ Name: proto.String("alice"), DisplayName: proto.String("Alice"), Password: proto.String("Alice's password"),
-
@@ -167,7 +167,7 @@ if err != nil {t.Fatal(err) } alice, ok := aliceRes.Msg.Result.(*workspaceV2.CreateUserResponse_Ok) alice, ok := aliceRes.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(aliceRes.Msg.Result)) t.Errorf("Expected ok, got %s", typeName.Type().Name())
-
@@ -177,9 +177,9 @@ if alice.Ok.GetName() != "alice" {t.Errorf("Expected name=alice, got %s", *alice.Ok.Name) } bobRes, err := client.CreateUser( bobRes, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ Name: proto.String("bob"), DisplayName: proto.String("Bob"), Password: proto.String("Bob's password"),
-
@@ -190,10 +190,10 @@ if err != nil {t.Fatal(err) } _, ok = bobRes.Msg.Result.(*workspaceV2.CreateUserResponse_AuthenticationError) _, ok = bobRes.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_PasswordExpired) if !ok { typeName := reflect.Indirect(reflect.ValueOf(bobRes.Msg.Result)) t.Errorf("Expected authentication_error, got %s", typeName.Type().Name()) t.Errorf("Expected password_expired, got %s", typeName.Type().Name()) } }
-
@@ -205,9 +205,9 @@ server.Client(),server.URL(), ) res, err := client.CreateUser( res, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ DisplayName: proto.String("Alice"), Password: proto.String("Alice's password"), InitialAdminPassword: proto.String("initial_admin_password"),
-
@@ -217,7 +217,7 @@ if err != nil {t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.CreateUserResponse_MissingFieldError) v, ok := res.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_MissingFieldError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Errorf("Expected missing_field_error, got %s", typeName.Type().Name())
-
@@ -236,9 +236,9 @@ server.Client(),server.URL(), ) res, err := client.CreateUser( res, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ Name: proto.String("alice"), DisplayName: proto.String("Alice"), InitialAdminPassword: proto.String("initial_admin_password"),
-
@@ -248,7 +248,7 @@ if err != nil {t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.CreateUserResponse_MissingFieldError) v, ok := res.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_MissingFieldError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Errorf("Expected missing_field_error, got %s", typeName.Type().Name())
-
@@ -267,9 +267,9 @@ server.Client(),server.URL(), ) res, err := client.CreateUser( res, err := client.CreateInitialAdmin( context.Background(), connect.NewRequest(&workspaceV2.CreateUserRequest{ connect.NewRequest(&workspaceV2.CreateInitialAdminRequest{ Name: proto.String("alice"), DisplayName: proto.String("Alice"), Password: proto.String("Alice's password"),
-
@@ -280,7 +280,7 @@ if err != nil {t.Fatal(err) } _, ok := res.Msg.Result.(*workspaceV2.CreateUserResponse_AuthenticationError) _, ok := res.Msg.Result.(*workspaceV2.CreateInitialAdminResponse_AuthenticationError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Errorf("Expected authentication_error, got %s", typeName.Type().Name())
-