From 1abb528198ad0c7430a5ff82f422f29c6a367255 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Thu, 23 Nov 2023 03:11:50 +1000 Subject: [PATCH] Refactor User manager service (#3117) Split off storage with ACL management. Added TTL cache for user records and gui options and setup TTL expiration processing. --- api/authenticators/basic.go | 10 +- api/authenticators/certs.go | 5 +- api/authenticators/google.go | 2 +- api/authenticators/saml.go | 2 +- api/users.go | 17 +- .../Server/Internal/UserManager.yaml | 4 + .../testdata/server/testcases/orgs.out.yaml | 20 - .../testdata/server/testcases/users.out.yaml | 2 +- bin/config.go | 4 +- bin/grant.go | 3 +- bin/orgs.go | 6 +- bin/users.go | 10 +- constants/constants.go | 1 + json/wrappers.go | 5 +- services/orgs.go | 2 +- services/orgs/delete.go | 10 +- services/orgs/services.go | 23 +- services/sanity/sanity_test.go | 2 + services/sanity/users.go | 8 +- services/users.go | 96 +++- {users => services/users}/add_org.go | 33 +- {users => services/users}/add_org_test.go | 37 +- services/users/delete.go | 76 ++- {users => services/users}/delete_test.go | 33 +- {users => services/users}/docs.go | 0 services/users/favorites.go | 31 +- .../users}/fixtures/TestAddUserToOrg.golden | 0 .../users}/fixtures/TestDeleteUser.golden | 2 +- .../users}/fixtures/TestGetUsers.golden | 0 .../users}/fixtures/TestListOrgs.golden | 0 .../users}/fixtures/TestListUsers.golden | 61 +-- .../users}/fixtures/TestMakeUsers.golden | 0 {users => services/users}/get.go | 37 +- {users => services/users}/get_test.go | 25 +- services/users/grpc.go | 27 +- services/users/list.go | 127 +++++ {users => services/users}/list_test.go | 27 +- {users => services/users}/orgs.go | 24 +- {users => services/users}/orgs_test.go | 10 +- {users => services/users}/set_user.go | 24 +- {users => services/users}/set_user_test.go | 13 +- services/users/storage.go | 504 ++++++++++++++++++ services/users/users.go | 239 +-------- {users => services/users}/users_test.go | 22 +- users/delete.go | 94 ---- users/list.go | 116 ---- utils/counter.go | 3 +- vql/server/orgs/delete.go | 4 +- vql/server/orgs/orgs.go | 4 +- vql/server/users/create.go | 6 +- vql/server/users/delete.go | 4 +- vql/server/users/get.go | 5 +- vql/server/users/grant.go | 3 +- vql/server/users/password.go | 5 +- vql/server/users/users.go | 6 +- 55 files changed, 1081 insertions(+), 753 deletions(-) create mode 100644 artifacts/definitions/Server/Internal/UserManager.yaml rename {users => services/users}/add_org.go (81%) rename {users => services/users}/add_org_test.go (69%) rename {users => services/users}/delete_test.go (66%) rename {users => services/users}/docs.go (100%) rename {users => services/users}/fixtures/TestAddUserToOrg.golden (100%) rename {users => services/users}/fixtures/TestDeleteUser.golden (96%) rename {users => services/users}/fixtures/TestGetUsers.golden (100%) rename {users => services/users}/fixtures/TestListOrgs.golden (100%) rename {users => services/users}/fixtures/TestListUsers.golden (80%) rename {users => services/users}/fixtures/TestMakeUsers.golden (100%) rename {users => services/users}/get.go (69%) rename {users => services/users}/get_test.go (69%) create mode 100644 services/users/list.go rename {users => services/users}/list_test.go (68%) rename {users => services/users}/orgs.go (65%) rename {users => services/users}/orgs_test.go (66%) rename {users => services/users}/set_user.go (87%) rename {users => services/users}/set_user_test.go (75%) create mode 100644 services/users/storage.go rename {users => services/users}/users_test.go (79%) delete mode 100644 users/delete.go delete mode 100644 users/list.go diff --git a/api/authenticators/basic.go b/api/authenticators/basic.go index 681d4f365ab..613a9633ed4 100644 --- a/api/authenticators/basic.go +++ b/api/authenticators/basic.go @@ -12,7 +12,6 @@ import ( "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/services/users" ) // Implement basic authentication. @@ -81,7 +80,8 @@ func (self *BasicAuthenticator) AuthenticateUserHandler( // Get the full user record with hashes so we can // verify it below. users_manager := services.GetUserManager() - user_record, err := users_manager.GetUserWithHashes(r.Context(), username) + user_record, err := users_manager.GetUserWithHashes(r.Context(), + username, username) if err != nil || user_record.Name != username { services.LogAudit(r.Context(), self.config_obj, username, "Unknown username", @@ -92,12 +92,14 @@ func (self *BasicAuthenticator) AuthenticateUserHandler( http.Error(w, "authorization failed", http.StatusUnauthorized) return } - - if !users.VerifyPassword(user_record, password) { + ok, err = users_manager.VerifyPassword(r.Context(), + username, username, password) + if !ok || err != nil { services.LogAudit(r.Context(), self.config_obj, username, "Invalid password", ordereddict.NewDict(). Set("remote", r.RemoteAddr). + Set("error", err.Error()). Set("status", http.StatusUnauthorized)) http.Error(w, "authorization failed", http.StatusUnauthorized) diff --git a/api/authenticators/certs.go b/api/authenticators/certs.go index 8c9cae9792f..cc442ce1e80 100644 --- a/api/authenticators/certs.go +++ b/api/authenticators/certs.go @@ -89,7 +89,6 @@ import ( "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" ) var ( @@ -166,7 +165,7 @@ func (self *CertAuthenticator) AuthenticateUserHandler( } users_manager := services.GetUserManager() - user_record, err := users_manager.GetUser(r.Context(), username) + user_record, err := users_manager.GetUser(r.Context(), username, username) if err != nil { if err != services.UserNotFoundError || len(self.default_roles) == 0 { http.Error(w, @@ -187,7 +186,7 @@ func (self *CertAuthenticator) AuthenticateUserHandler( // Use the super user principal to actually add the // username so we have enough permissions. - err = users.AddUserToOrg(r.Context(), users.AddNewUser, + err = users_manager.AddUserToOrg(r.Context(), services.AddNewUser, constants.PinnedServerName, username, []string{"root"}, policy) if err != nil { diff --git a/api/authenticators/google.go b/api/authenticators/google.go index 079ff8139ec..dbb6582e772 100644 --- a/api/authenticators/google.go +++ b/api/authenticators/google.go @@ -288,7 +288,7 @@ func authenticateUserHandle( // Now check if the user is allowed to log in. users := services.GetUserManager() - user_record, err := users.GetUser(r.Context(), username) + user_record, err := users.GetUser(r.Context(), username, username) if err != nil || user_record.Name != username { reject_cb(w, r, errors.New("Invalid user"), username) return diff --git a/api/authenticators/saml.go b/api/authenticators/saml.go index 7b04c4d87e4..13551c77828 100644 --- a/api/authenticators/saml.go +++ b/api/authenticators/saml.go @@ -112,7 +112,7 @@ func (self *SamlAuthenticator) AuthenticateUserHandler( username := sa.GetAttributes().Get(self.user_attribute) users := services.GetUserManager() - user_record, err := users.GetUser(r.Context(), username) + user_record, err := users.GetUser(r.Context(), username, username) if err == nil && user_record.Name == username { // Does the user have access to the specified org? err = CheckOrgAccess(r, user_record) diff --git a/api/users.go b/api/users.go index 9b4df9b4a9d..978461242b3 100644 --- a/api/users.go +++ b/api/users.go @@ -13,7 +13,6 @@ import ( acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" ) // This is only used to set the user's own password which is always @@ -50,7 +49,7 @@ func (self *ApiServer) SetPassword( target = principal } - err = users.SetUserPassword( + err = user_manager.SetUserPassword( ctx, org_config_obj, principal, target, in.Password, "") if err != nil { return nil, Status(self.verbose, err) @@ -72,7 +71,7 @@ func (self *ApiServer) GetUsers( principal := user_record.Name // Only show users in the current org - users, err := users.ListUsers(ctx, principal, []string{org_config_obj.OrgId}) + users, err := user_manager.ListUsers(ctx, principal, []string{org_config_obj.OrgId}) if err != nil { return nil, Status(self.verbose, err) } @@ -94,7 +93,7 @@ func (self *ApiServer) GetGlobalUsers( principal := user_record.Name // Show all users visible to us - users, err := users.ListUsers(ctx, principal, []string{}) + users, err := user_manager.ListUsers(ctx, principal, []string{}) if err != nil { return nil, Status(self.verbose, err) } @@ -120,12 +119,12 @@ func (self *ApiServer) CreateUser(ctx context.Context, Roles: in.Roles, } - mode := users.UseExistingUser + mode := services.UseExistingUser if in.AddNewUser { - mode = users.AddNewUser + mode = services.AddNewUser } - err = users.AddUserToOrg(ctx, mode, principal, in.Name, in.Orgs, acl) + err = users_manager.AddUserToOrg(ctx, mode, principal, in.Name, in.Orgs, acl) if err == nil { services.LogAudit(ctx, @@ -148,7 +147,7 @@ func (self *ApiServer) GetUser( return nil, err } - user, err := users.GetUser(ctx, user_record.Name, in.Name) + user, err := users_manager.GetUser(ctx, user_record.Name, in.Name) if err != nil { if errors.Is(err, acls.PermissionDenied) { return nil, status.Error(codes.PermissionDenied, @@ -255,7 +254,7 @@ func (self *ApiServer) SetUserRoles( // Now attempt to set the ACL - permission checks are done by // users.AddUserToOrg - err = users.AddUserToOrg(ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg(ctx, services.UseExistingUser, principal, in.Name, []string{in.Org}, acl) if err == nil { diff --git a/artifacts/definitions/Server/Internal/UserManager.yaml b/artifacts/definitions/Server/Internal/UserManager.yaml new file mode 100644 index 00000000000..d6c339a5908 --- /dev/null +++ b/artifacts/definitions/Server/Internal/UserManager.yaml @@ -0,0 +1,4 @@ +name: Server.Internal.UserManager +type: INTERNAL +description: | + An internal artifact notifying when user accounts are modified. diff --git a/artifacts/testdata/server/testcases/orgs.out.yaml b/artifacts/testdata/server/testcases/orgs.out.yaml index c6d1edb2ec8..c1e26a2ddbd 100644 --- a/artifacts/testdata/server/testcases/orgs.out.yaml +++ b/artifacts/testdata/server/testcases/orgs.out.yaml @@ -59,16 +59,6 @@ SELECT Name, OrgId FROM orgs() ORDER BY OrgId[ "delete_results": true } }, - { - "name": "OrgAdmin", - "org_id": "ORGID", - "org_name": "MyOrg", - "picture": "", - "email": false, - "roles": null, - "_policy": {}, - "effective_policy": null - }, { "name": "OrgUser", "org_id": "ORGID", @@ -196,16 +186,6 @@ SELECT Name, OrgId FROM orgs() ORDER BY OrgId[ } } ]SELECT * FROM query(query={ SELECT * FROM gui_users() ORDER BY name }, org_id="ORGID")[ - { - "name": "OrgAdmin", - "org_id": "ORGID", - "org_name": "MyOrg", - "picture": "", - "email": false, - "roles": null, - "_policy": {}, - "effective_policy": null - }, { "name": "OrgUser", "org_id": "ORGID", diff --git a/artifacts/testdata/server/testcases/users.out.yaml b/artifacts/testdata/server/testcases/users.out.yaml index 0a85c860c5e..3e2c84616b6 100644 --- a/artifacts/testdata/server/testcases/users.out.yaml +++ b/artifacts/testdata/server/testcases/users.out.yaml @@ -172,6 +172,6 @@ SELECT whoami() FROM scope()[ } ]SELECT * FROM test_read_logs() WHERE Log =~ "Username is reserved" AND NOT Log =~ "SELECT"[ { - "Log": "Velociraptor: user_create: Username is reserved\n" + "Log": "Velociraptor: user_create: Username is reserved: VelociraptorServer\n" } ] \ No newline at end of file diff --git a/bin/config.go b/bin/config.go index 43d7d41243c..55061b79c44 100644 --- a/bin/config.go +++ b/bin/config.go @@ -34,6 +34,7 @@ import ( api_proto "www.velocidex.com/golang/velociraptor/api/proto" "www.velocidex.com/golang/velociraptor/config" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/crypto" "www.velocidex.com/golang/velociraptor/crypto/utils" "www.velocidex.com/golang/velociraptor/json" @@ -550,7 +551,8 @@ func doDumpApiClientConfig() error { // Make sure the user actually exists. user_manager := services.GetUserManager() - _, err = user_manager.GetUser(ctx, *config_api_client_common_name) + _, err = user_manager.GetUser(ctx, constants.PinnedServerName, + *config_api_client_common_name) if err != nil { // Need to ensure we have a user err := user_manager.SetUser(ctx, &api_proto.VelociraptorUser{ diff --git a/bin/grant.go b/bin/grant.go index fa5eaa25d92..debdec11e51 100644 --- a/bin/grant.go +++ b/bin/grant.go @@ -8,6 +8,7 @@ import ( jsonpatch "github.com/evanphx/json-patch/v5" "www.velocidex.com/golang/velociraptor/acls" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" logging "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" @@ -78,7 +79,7 @@ func doGrant() error { // Check the user actually exists first user_manager := services.GetUserManager() - _, err = user_manager.GetUser(ctx, principal) + _, err = user_manager.GetUser(ctx, constants.PinnedServerName, principal) if err != nil { return err } diff --git a/bin/orgs.go b/bin/orgs.go index 378f9f278df..4fc53ccc7f5 100644 --- a/bin/orgs.go +++ b/bin/orgs.go @@ -4,6 +4,7 @@ import ( "fmt" api_proto "www.velocidex.com/golang/velociraptor/api/proto" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" logging "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" @@ -87,7 +88,7 @@ func doOrgUserAdd() error { user_manager := services.GetUserManager() record, err := user_manager.GetUserWithHashes( - ctx, *orgs_user_add_user) + ctx, constants.PinnedServerName, *orgs_user_add_user) if err != nil { return err } @@ -179,7 +180,8 @@ func doOrgDelete() error { logger := logging.GetLogger(config_obj, &logging.ToolComponent) logger.Info("Will remove org %v\n", *orgs_delete_org_id) - return org_manager.DeleteOrg(ctx, *orgs_delete_org_id) + return org_manager.DeleteOrg(ctx, + constants.PinnedServerName, *orgs_delete_org_id) } func init() { diff --git a/bin/users.go b/bin/users.go index 4ce1162df49..f4c661aedfb 100644 --- a/bin/users.go +++ b/bin/users.go @@ -25,6 +25,7 @@ import ( "golang.org/x/crypto/ssh/terminal" "www.velocidex.com/golang/velociraptor/api/authenticators" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/json" logging "www.velocidex.com/golang/velociraptor/logging" "www.velocidex.com/golang/velociraptor/services" @@ -152,13 +153,15 @@ func doShowUser() error { defer sm.Close() users_manager := services.GetUserManager() - user_record, err := users_manager.GetUser(ctx, *user_show_name) + user_record, err := users_manager.GetUser(ctx, constants.PinnedServerName, + *user_show_name) if err != nil { return err } if *user_show_hashes { - user_record, err := users_manager.GetUserWithHashes(ctx, *user_show_name) + user_record, err := users_manager.GetUserWithHashes(ctx, + constants.PinnedServerName, *user_show_name) if err != nil { return err } @@ -197,7 +200,8 @@ func doLockUser() error { defer sm.Close() users_manager := services.GetUserManager() - user_record, err := users_manager.GetUser(ctx, *user_lock_name) + user_record, err := users_manager.GetUser(ctx, constants.PinnedServerName, + *user_lock_name) if err != nil { return fmt.Errorf("Unable to find user %s", *user_lock_name) } diff --git a/constants/constants.go b/constants/constants.go index 7b3a3aa91fd..3c5b412722e 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -113,6 +113,7 @@ const ( SQLITE_ALWAYS_MAKE_TEMPFILE = "SQLITE_ALWAYS_MAKE_TEMPFILE" PinnedServerName = "VelociraptorServer" + PinnedGwName = "GRPC_GW" CLIENT_API_VERSION = uint32(4) diff --git a/json/wrappers.go b/json/wrappers.go index 89c33bc0cfe..1a362832afd 100644 --- a/json/wrappers.go +++ b/json/wrappers.go @@ -125,7 +125,10 @@ func Unmarshal(b []byte, v interface{}) error { self, ok := v.(proto.Message) if ok { - return protojson.Unmarshal(b, self) + options := &protojson.UnmarshalOptions{ + DiscardUnknown: true, + } + return options.Unmarshal(b, self) } return json.Unmarshal(b, v) diff --git a/services/orgs.go b/services/orgs.go index aa83d59b1e1..6f992c6db25 100644 --- a/services/orgs.go +++ b/services/orgs.go @@ -71,7 +71,7 @@ type OrgManager interface { CreateNewOrg(name, id string) (*api_proto.OrgRecord, error) ListOrgs() []*api_proto.OrgRecord GetOrg(org_id string) (*api_proto.OrgRecord, error) - DeleteOrg(ctx context.Context, org_id string) error + DeleteOrg(ctx context.Context, principal, org_id string) error // The manager is responsible for running multiple services - one // for each org. This ensures org services are separated out and diff --git a/services/orgs/delete.go b/services/orgs/delete.go index 879820aa455..d0375e37a42 100644 --- a/services/orgs/delete.go +++ b/services/orgs/delete.go @@ -16,17 +16,17 @@ import ( ) func RemoveOrgFromUsers( - ctx context.Context, org_id string) error { + ctx context.Context, principal, org_id string) error { // Remove the org from all the users. user_manager := services.GetUserManager() - users, err := user_manager.ListUsers(ctx) + users, err := user_manager.ListUsers(ctx, principal, services.LIST_ALL_ORGS) if err != nil { return err } for _, u := range users { - record, err := user_manager.GetUserWithHashes(ctx, u.Name) + record, err := user_manager.GetUserWithHashes(ctx, principal, u.Name) if err == nil { new_orgs := []*api_proto.OrgRecord{} for _, org := range record.Orgs { @@ -44,12 +44,12 @@ func RemoveOrgFromUsers( return nil } -func (self *OrgManager) DeleteOrg(ctx context.Context, org_id string) error { +func (self *OrgManager) DeleteOrg(ctx context.Context, principal, org_id string) error { if utils.IsRootOrg(org_id) { return errors.New("Can not remove root org.") } - err := RemoveOrgFromUsers(ctx, org_id) + err := RemoveOrgFromUsers(ctx, principal, org_id) if err != nil { return err } diff --git a/services/orgs/services.go b/services/orgs/services.go index cbc189221fb..876de167550 100644 --- a/services/orgs/services.go +++ b/services/orgs/services.go @@ -377,6 +377,17 @@ func (self *OrgManager) startOrgFromContext(org_ctx *OrgContext) (err error) { service_container.mu.Unlock() } + if spec.JournalService { + j, err := journal.NewJournalService(ctx, wg, org_config) + if err != nil { + return err + } + service_container.mu.Lock() + service_container.journal = j + service_container.broadcast = broadcast.NewBroadcastService(org_config) + service_container.mu.Unlock() + } + // Now start service on the root org if utils.IsRootOrg(org_id) { err := self.startRootOrgServices(ctx, wg, spec, org_config, service_container) @@ -385,6 +396,7 @@ func (self *OrgManager) startOrgFromContext(org_ctx *OrgContext) (err error) { } } + // ACL manager exist for each org if spec.UserManager { m, err := acl_manager.NewACLManager(ctx, wg, org_config) if err != nil { @@ -399,17 +411,6 @@ func (self *OrgManager) startOrgFromContext(org_ctx *OrgContext) (err error) { // Now start the services for this org. Services depend on other // services so they need to be accessible as soon as they are // ready. - if spec.JournalService { - j, err := journal.NewJournalService(ctx, wg, org_config) - if err != nil { - return err - } - service_container.mu.Lock() - service_container.journal = j - service_container.broadcast = broadcast.NewBroadcastService(org_config) - service_container.mu.Unlock() - } - if spec.NotificationService { n, err := notifications.NewNotificationService(ctx, wg, org_config) if err != nil { diff --git a/services/sanity/sanity_test.go b/services/sanity/sanity_test.go index 2ebfa823a8d..2dcc64c8272 100644 --- a/services/sanity/sanity_test.go +++ b/services/sanity/sanity_test.go @@ -41,6 +41,8 @@ tools: - name: Tool2 url: https://www.example2.com/ +`, `name: Server.Internal.UserManager +type: INTERNAL `}) self.ConfigObj.Services.NotebookService = true self.ConfigObj.Services.UserManager = true diff --git a/services/sanity/users.go b/services/sanity/users.go index edf13c8cfb1..07f3276cec7 100644 --- a/services/sanity/users.go +++ b/services/sanity/users.go @@ -21,6 +21,11 @@ func createInitialUsers( return nil } + superuser := "VelociraptorServer" + if config_obj.Client != nil { + superuser = config_obj.Client.PinnedServerName + } + user_names := config_obj.GUI.InitialUsers logger := logging.GetLogger(config_obj, &logging.FrontendComponent) @@ -30,6 +35,7 @@ func createInitialUsers( for _, org := range config_obj.GUI.InitialOrgs { org_list = append(org_list, org.OrgId) } + org_manager, err := services.GetOrgManager() if err != nil { return err @@ -37,7 +43,7 @@ func createInitialUsers( for _, user := range user_names { users_manager := services.GetUserManager() - user_record, err := users_manager.GetUser(ctx, user.Name) + user_record, err := users_manager.GetUser(ctx, superuser, user.Name) if err != nil || user_record.Name != user.Name { logger.Info("Initial user %v not present, creating", user.Name) new_user, err := users.NewUserRecord(config_obj, user.Name) diff --git a/services/users.go b/services/users.go index 7d958520220..727889157b0 100644 --- a/services/users.go +++ b/services/users.go @@ -21,6 +21,7 @@ import ( "context" "errors" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" ) @@ -29,28 +30,109 @@ var ( global_user_manager UserManager UserNotFoundError = errors.New("User not found") + + LIST_ALL_ORGS []string = nil +) + +type AddUserOptions int + +const ( + UseExistingUser AddUserOptions = iota + AddNewUser ) +/* + The user manager is global to all orgs and therefore it is + initialized once for the root org. + + This is the one stop shop for managing everything about users except + ACLs (which are managed within in org seperately) +*/ type UserManager interface { SetUser(ctx context.Context, user_record *api_proto.VelociraptorUser) error - GetUser(ctx context.Context, - username string) (*api_proto.VelociraptorUser, error) - ListUsers(ctx context.Context) ([]*api_proto.VelociraptorUser, error) + GetUser(ctx context.Context, principal, username string) ( + *api_proto.VelociraptorUser, error) + + // Verify the User password (only used for Basic Authentication. + VerifyPassword( + ctx context.Context, + principal, username string, + password string) (bool, error) + + // List all users in these orgs. + ListUsers(ctx context.Context, + principal string, orgs []string) ([]*api_proto.VelociraptorUser, error) + GetUserFromContext(ctx context.Context) ( *api_proto.VelociraptorUser, *config_proto.Config, error) - GetUserWithHashes(ctx context.Context, username string) (*api_proto.VelociraptorUser, error) + // Used to get the user's record including password hashes. This + // only makes sense when using the `Basic` authenticator because + // otherwise we dont maintain passwords. + GetUserWithHashes(ctx context.Context, principal, username string) ( + *api_proto.VelociraptorUser, error) + + // Used to set and retrieve user GUI options. SetUserOptions(ctx context.Context, username string, options *api_proto.SetGUIOptionsRequest) error - GetUserOptions(ctx context.Context, username string) (*api_proto.SetGUIOptionsRequest, error) - // Favorites are stored per org. + GetUserOptions(ctx context.Context, username string) ( + *api_proto.SetGUIOptionsRequest, error) + + // Favorites are stored per org because they refer to artifacts + // which may be specific for each org. GetFavorites(ctx context.Context, config_obj *config_proto.Config, principal, fav_type string) (*api_proto.Favorites, error) - DeleteUser(ctx context.Context, config_obj *config_proto.Config, username string) error + // List all the orgs the user can see (i.e the users has READER + // level access) + GetOrgs(ctx context.Context, principal string) []*api_proto.OrgRecord + + // Adds the user to the org. + // - OrgAdmin can add any user to any org. + // - ServerAdmin is required in all orgs. + + // If the user account does not exist and the AddNewUser option is + // provided, we create a new user account. + + // This function effectively grants permissions in the org so it is + // the same as GrantUserInOrg + AddUserToOrg(ctx context.Context, + options AddUserOptions, + principal, username string, + orgs []string, policy *acl_proto.ApiClientACL) error + + // Update the user's password. + // A user may update their own password. + // A ServerAdmin in any of the orgs the user belongs to can update their password. + // An OrgAdmin can update everyone's password + SetUserPassword( + ctx context.Context, + org_config_obj *config_proto.Config, + principal, username string, + password, current_org string) error + + // Removes the user record. + // principal - is the user who is requesting this account removal. + // username - the user to remove. + // orgs - The list of orgs to remove the user from. + DeleteUser( + ctx context.Context, + principal, username string, + orgs []string) error +} + +// A helper +func GrantUserToOrg( + ctx context.Context, + principal, username string, + orgs []string, policy *acl_proto.ApiClientACL) error { + + users_manager := GetUserManager() + return users_manager.AddUserToOrg(ctx, UseExistingUser, + principal, username, orgs, policy) } func RegisterUserManager(dispatcher UserManager) { diff --git a/users/add_org.go b/services/users/add_org.go similarity index 81% rename from users/add_org.go rename to services/users/add_org.go index cbb51967954..e6630e4cf85 100644 --- a/users/add_org.go +++ b/services/users/add_org.go @@ -10,13 +10,6 @@ import ( "www.velocidex.com/golang/velociraptor/services" ) -type AddUserOptions int - -const ( - UseExistingUser AddUserOptions = iota - AddNewUser -) - // Adds the user to the org. // - OrgAdmin can add any user to any org. // - ServerAdmin is required in all orgs. @@ -26,14 +19,15 @@ const ( // This function effectively grants permissions in the org so it is // the same as GrantUserInOrg -func AddUserToOrg( +func (self *UserManager) AddUserToOrg( ctx context.Context, - options AddUserOptions, + options services.AddUserOptions, principal, username string, orgs []string, policy *acl_proto.ApiClientACL) error { - if isNameReserved(username) { - return NameReservedError + err := validateUsername(self.config_obj, username) + if err != nil { + return err } org_manager, err := services.GetOrgManager() @@ -64,13 +58,14 @@ func AddUserToOrg( } } - user_manager := services.GetUserManager() - - // Hold on to the error until after ACL check - user_record, err := user_manager.GetUserWithHashes(ctx, username) + // Hold on to the error until after ACL check. Get the full + // unfiltered user record with all the orgs they belong to so we + // can remove those orgs the principal is allowed to touch and put + // the rest back. + user_record, err := self.storage.GetUserWithHashes(ctx, username) if err != nil { if err == services.UserNotFoundError && - options == UseExistingUser { + options == services.UseExistingUser { return err } @@ -101,14 +96,16 @@ func AddUserToOrg( } - return user_manager.SetUser(ctx, user_record) + return self.SetUser(ctx, user_record) } func GrantUserToOrg( ctx context.Context, principal, username string, orgs []string, policy *acl_proto.ApiClientACL) error { - return AddUserToOrg(ctx, UseExistingUser, + + user_manager := services.GetUserManager() + return user_manager.AddUserToOrg(ctx, services.UseExistingUser, principal, username, orgs, policy) } diff --git a/users/add_org_test.go b/services/users/add_org_test.go similarity index 69% rename from users/add_org_test.go rename to services/users/add_org_test.go index 7ee2ecd43d6..85fc4dd74f5 100644 --- a/users/add_org_test.go +++ b/services/users/add_org_test.go @@ -5,7 +5,7 @@ import ( "github.com/sebdah/goldie" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/vtesting/assert" ) @@ -23,59 +23,60 @@ func (self *UserManagerTestSuite) TestAddUserToOrg() { } // Can a simple user add themselves to another org? - err := users.AddUserToOrg( - self.Ctx, users.UseExistingUser, + users_manager := services.GetUserManager() + err := users_manager.AddUserToOrg( + self.Ctx, services.UseExistingUser, "UserO1", "UserO1", []string{"O2"}, admin_policy) assert.ErrorContains(self.T(), err, "PermissionDenied") // Can an admin in O1 just add a user to O2? - err = users.AddUserToOrg( - self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg( + self.Ctx, services.UseExistingUser, "AdminO1", "UserO1", []string{"O2"}, admin_policy) assert.ErrorContains(self.T(), err, "PermissionDenied") // Can an OrgAdmin add a user from O1 to O2? - err = users.AddUserToOrg( - self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg( + self.Ctx, services.UseExistingUser, "OrgAdmin", "AdminO1", []string{"O2"}, admin_policy) assert.NoError(self.T(), err) - user_record, err := users.GetUser(self.Ctx, "OrgAdmin", "AdminO1") + user_record, err := users_manager.GetUser(self.Ctx, "OrgAdmin", "AdminO1") assert.NoError(self.T(), err) golden.Set("AdminO1 belongs in O1 and O2", user_record) // Now AdminO1 is an admin in both O1 and O2 so they can add the // user there. - err = users.AddUserToOrg( - self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg( + self.Ctx, services.UseExistingUser, "AdminO1", "UserO1", []string{"O2"}, reader_policy) assert.NoError(self.T(), err) - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO1") assert.NoError(self.T(), err) golden.Set("UserO1 belongs in O1 and O2", user_record) // Try to add an unknown user. - err = users.AddUserToOrg( - self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg( + self.Ctx, services.UseExistingUser, "OrgAdmin", "NoSuchUser", []string{"O2"}, admin_policy) assert.ErrorContains(self.T(), err, "User not found") // Request a new user record to be created. - err = users.AddUserToOrg( - self.Ctx, users.AddNewUser, + err = users_manager.AddUserToOrg( + self.Ctx, services.AddNewUser, "AdminO2", "NoSuchUser", []string{"O2"}, reader_policy) assert.NoError(self.T(), err) - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "NoSuchUser") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "NoSuchUser") assert.NoError(self.T(), err) golden.Set("New Users NoSuchUser", user_record) // Try to create a reserved user - err = users.AddUserToOrg( - self.Ctx, users.AddNewUser, + err = users_manager.AddUserToOrg( + self.Ctx, services.AddNewUser, "AdminO2", "VelociraptorServer", []string{"O2"}, reader_policy) assert.ErrorContains(self.T(), err, "reserved") diff --git a/services/users/delete.go b/services/users/delete.go index 1dfd55b65f0..4921bea3b00 100644 --- a/services/users/delete.go +++ b/services/users/delete.go @@ -3,56 +3,96 @@ package users import ( "context" - config_proto "www.velocidex.com/golang/velociraptor/config/proto" - datastore "www.velocidex.com/golang/velociraptor/datastore" - "www.velocidex.com/golang/velociraptor/paths" + "github.com/pkg/errors" + "www.velocidex.com/golang/velociraptor/acls" + acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" ) +// Removes a user from an org func (self *UserManager) DeleteUser( ctx context.Context, - org_config_obj *config_proto.Config, username string) error { + principal, username string, + orgs []string) error { - err := validateUsername(org_config_obj, username) + err := validateUsername(self.config_obj, username) if err != nil { return err } + // Hold on to the error until after ACL check. Get the full + // unfiltered user record with all the orgs they belong to so we + // can remove those orgs the principal is allowed to touch and put + // the rest back. + user_record, user_err := self.storage.GetUserWithHashes(ctx, username) + org_manager, err := services.GetOrgManager() if err != nil { return err } - // Get the root org config because users are managed in the root - // org. root_config_obj, err := org_manager.GetOrgConfig(services.ROOT_ORG_ID) if err != nil { return err } - db, err := datastore.GetDB(root_config_obj) + principal_is_org_admin, err := services.CheckAccess( + root_config_obj, principal, acls.ORG_ADMIN) if err != nil { return err } - user_path_manager := paths.NewUserPathManager(username) - err = db.DeleteSubject(root_config_obj, user_path_manager.Path()) - if err != nil { - return err + if user_err != nil { + if principal_is_org_admin { + return user_err + } + return errors.Errorf("Error %v: User %v is not org admin", + acls.PermissionDenied, principal) } - // Also remove the ACLs for the user from all orgs. - for _, org_record := range org_manager.ListOrgs() { - org_config_obj, err := org_manager.GetOrgConfig(org_record.Id) + remaining_orgs := []*api_proto.OrgRecord{} + // Empty policy - no permissions. + policy := &acl_proto.ApiClientACL{} + + for _, user_org := range user_record.Orgs { + org_config_obj, err := org_manager.GetOrgConfig(user_org.Id) if err != nil { + remaining_orgs = append(remaining_orgs, user_org) continue } - err = db.DeleteSubject(org_config_obj, user_path_manager.ACL()) - if err != nil { + // Skip orgs that are not specified. + if len(orgs) > 0 && !utils.OrgIdInList(user_org.Id, orgs) { + remaining_orgs = append(remaining_orgs, user_org) continue } + + // Further checks if the principal is not ORG_ADMIN + if !principal_is_org_admin { + ok, _ := services.CheckAccess( + org_config_obj, principal, acls.SERVER_ADMIN) + if !ok { + // If the user is not server admin on this org they + // may not remove the user from this org + remaining_orgs = append(remaining_orgs, user_org) + continue + } + } + + // Reset the user's ACLs in this org. + err = services.SetPolicy(org_config_obj, username, policy) + if err != nil { + return err + } + } + + if len(remaining_orgs) > 0 { + // Update the user's record + user_record.Orgs = remaining_orgs + return self.SetUser(ctx, user_record) } - return nil + return self.storage.DeleteUser(ctx, username) } diff --git a/users/delete_test.go b/services/users/delete_test.go similarity index 66% rename from users/delete_test.go rename to services/users/delete_test.go index 0c9ee6e8658..a95c709bc30 100644 --- a/users/delete_test.go +++ b/services/users/delete_test.go @@ -5,7 +5,7 @@ import ( "github.com/sebdah/goldie" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/vtesting/assert" ) @@ -16,21 +16,22 @@ func (self *UserManagerTestSuite) TestDeleteUser() { // Can a user remove their own account? No but we just ignore // their request. - err := users.DeleteUser( + users_manager := services.GetUserManager() + err := users_manager.DeleteUser( self.Ctx, "UserO1", "UserO1", []string{"O1"}) assert.NoError(self.T(), err) - user_record, err := users.GetUser(self.Ctx, "OrgAdmin", "UserO1") + user_record, err := users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO1") assert.NoError(self.T(), err) - golden.Set("UserO1 delete UserO1", user_record) + golden.Set("OrgAdmin GetUser UserO1", user_record) // Can AdminO1 remove a user in their org? - err = users.DeleteUser( + err = users_manager.DeleteUser( self.Ctx, "AdminO1", "UserO1", []string{"O1"}) assert.NoError(self.T(), err) // Yes user is gone. - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO1") assert.ErrorContains(self.T(), err, "User not found") // UserO2 belongs in both O1 and O2 @@ -38,44 +39,46 @@ func (self *UserManagerTestSuite) TestDeleteUser() { Roles: []string{"reader"}, } - err = users.AddUserToOrg(self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg(self.Ctx, services.UseExistingUser, "OrgAdmin", "UserO2", []string{"O1", "O2"}, reader_policy) assert.NoError(self.T(), err) // Lookup using ORG_ADMIN - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO2") assert.NoError(self.T(), err) golden.Set("OrgAdmin UserO2 is in O1 and O2", user_record) // Lookup using O1's SERVER_ADMIN - user_record, err = users.GetUser(self.Ctx, "AdminO1", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "UserO2") assert.NoError(self.T(), err) golden.Set("AdminO1 UserO2 is in O1", user_record) // Lookup using O2's SERVER_ADMIN - user_record, err = users.GetUser(self.Ctx, "AdminO2", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO2", "UserO2") assert.NoError(self.T(), err) golden.Set("AdminO2 UserO2 is in O2", user_record) // AdminO2 will remove the user from all orgs, but they remain in // O1 because AdminO2 has no accesss to O1 - err = users.DeleteUser( - self.Ctx, "AdminO2", "UserO2", users.LIST_ALL_ORGS) + err = users_manager.DeleteUser( + self.Ctx, "AdminO2", "UserO2", services.LIST_ALL_ORGS) assert.NoError(self.T(), err) // GetUser returns PermissionDenied if the user requesting does // not have OrgAdmin and does not belong to any of the same orgs - user_record, err = users.GetUser(self.Ctx, "AdminO2", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO2", "UserO2") assert.ErrorContains(self.T(), err, "PermissionDenied") golden.Set("AdminO2 UserO2 removed from O2", err.Error()) + // test_utils.GetMemoryDataStore(self.T(), self.ConfigObj).Debug(self.ConfigObj) + // If the user was added to O1 and removed from O2, it should // still exist in O1 - user_record, err = users.GetUser(self.Ctx, "AdminO1", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "UserO2") assert.NoError(self.T(), err) golden.Set("AdminO1 UserO2 still in O1", user_record) - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO2") assert.NoError(self.T(), err) golden.Set("OrgAdmin UserO2 removed from O2", user_record) diff --git a/users/docs.go b/services/users/docs.go similarity index 100% rename from users/docs.go rename to services/users/docs.go diff --git a/services/users/favorites.go b/services/users/favorites.go index 6e87b28d286..539b5796de7 100644 --- a/services/users/favorites.go +++ b/services/users/favorites.go @@ -5,40 +5,11 @@ import ( api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - datastore "www.velocidex.com/golang/velociraptor/datastore" - "www.velocidex.com/golang/velociraptor/paths" ) func (self UserManager) GetFavorites( ctx context.Context, config_obj *config_proto.Config, principal, fav_type string) (*api_proto.Favorites, error) { - result := &api_proto.Favorites{} - path_manager := paths.NewUserPathManager(principal) - - db, err := datastore.GetDB(config_obj) - if err != nil { - return nil, err - } - - components := path_manager.FavoriteDir(fav_type) - children, err := db.ListChildren(config_obj, components) - if err != nil { - return nil, err - } - - for _, child := range children { - if child.IsDir() { - continue - } - - fav := &api_proto.Favorite{} - err = db.GetSubject(config_obj, - path_manager.Favorites(child.Base(), fav_type), fav) - if err == nil { - result.Items = append(result.Items, fav) - } - } - - return result, nil + return self.storage.GetFavorites(ctx, config_obj, principal, fav_type) } diff --git a/users/fixtures/TestAddUserToOrg.golden b/services/users/fixtures/TestAddUserToOrg.golden similarity index 100% rename from users/fixtures/TestAddUserToOrg.golden rename to services/users/fixtures/TestAddUserToOrg.golden diff --git a/users/fixtures/TestDeleteUser.golden b/services/users/fixtures/TestDeleteUser.golden similarity index 96% rename from users/fixtures/TestDeleteUser.golden rename to services/users/fixtures/TestDeleteUser.golden index 5678e32e602..dfe4bd42eaf 100644 --- a/users/fixtures/TestDeleteUser.golden +++ b/services/users/fixtures/TestDeleteUser.golden @@ -1,5 +1,5 @@ { - "UserO1 delete UserO1": { + "OrgAdmin GetUser UserO1": { "name": "UserO1", "orgs": [ { diff --git a/users/fixtures/TestGetUsers.golden b/services/users/fixtures/TestGetUsers.golden similarity index 100% rename from users/fixtures/TestGetUsers.golden rename to services/users/fixtures/TestGetUsers.golden diff --git a/users/fixtures/TestListOrgs.golden b/services/users/fixtures/TestListOrgs.golden similarity index 100% rename from users/fixtures/TestListOrgs.golden rename to services/users/fixtures/TestListOrgs.golden diff --git a/users/fixtures/TestListUsers.golden b/services/users/fixtures/TestListUsers.golden similarity index 80% rename from users/fixtures/TestListUsers.golden rename to services/users/fixtures/TestListUsers.golden index bf41e8cc32f..469a3a270e6 100644 --- a/users/fixtures/TestListUsers.golden +++ b/services/users/fixtures/TestListUsers.golden @@ -24,14 +24,6 @@ { "name": "\u003croot\u003e", "id": "root" - }, - { - "name": "O1", - "id": "O1" - }, - { - "name": "O2", - "id": "O2" } ] }, @@ -65,36 +57,38 @@ ] }, { - "name": "OrgAdmin", + "name": "UserO1", "orgs": [ { "name": "O1", "id": "O1" } ] - }, + } + ], + "AdminO1 ListUsers": [ { - "name": "UserO1", + "name": "AdminO1", "orgs": [ { "name": "O1", "id": "O1" } ] - } - ], - "AdminO1 ListUsers": [ + }, { - "name": "AdminO1", + "name": "UserO1", "orgs": [ { "name": "O1", "id": "O1" } ] - }, + } + ], + "UserO1 ListUsers": [ { - "name": "OrgAdmin", + "name": "AdminO1", "orgs": [ { "name": "O1", @@ -112,17 +106,6 @@ ] } ], - "UserO1 ListUsers": [ - { - "name": "AdminO1" - }, - { - "name": "OrgAdmin" - }, - { - "name": "UserO1" - } - ], "AdminO2 ListUsers - Filtered AdminO1 Orgs": [ { "name": "AdminO1", @@ -142,15 +125,6 @@ } ] }, - { - "name": "OrgAdmin", - "orgs": [ - { - "name": "O2", - "id": "O2" - } - ] - }, { "name": "UserO2", "orgs": [ @@ -184,19 +158,6 @@ } ] }, - { - "name": "OrgAdmin", - "orgs": [ - { - "name": "O1", - "id": "O1" - }, - { - "name": "O2", - "id": "O2" - } - ] - }, { "name": "UserO1", "orgs": [ diff --git a/users/fixtures/TestMakeUsers.golden b/services/users/fixtures/TestMakeUsers.golden similarity index 100% rename from users/fixtures/TestMakeUsers.golden rename to services/users/fixtures/TestMakeUsers.golden diff --git a/users/get.go b/services/users/get.go similarity index 69% rename from users/get.go rename to services/users/get.go index 36441bef695..1b69e9d6d01 100644 --- a/users/get.go +++ b/services/users/get.go @@ -8,28 +8,41 @@ import ( "www.velocidex.com/golang/velociraptor/services" ) +// Returns the user record after stripping sensitive information like +// password hashes. + // Gets the user record. // If the principal == username the user is getting their own record. // If the principal is a SERVER_ADMIN in any orgs the user belongs in // they can see the full record. // If the principal has READER in any orgs the user belongs in they // can only see the user name. -func GetUser( - ctx context.Context, - principal, username string) (*api_proto.VelociraptorUser, error) { +func (self *UserManager) GetUser( + ctx context.Context, principal, username string) (*api_proto.VelociraptorUser, error) { + + // For the server name we dont have a real user record, we make a + // hard coded user record instead. + if username == self.config_obj.Client.PinnedServerName { + return &api_proto.VelociraptorUser{ + Name: username, + }, nil + } - result, err := getUserWithHashes(ctx, principal, username) + // Call our overloaded method which check permissions and + // visibility. + result, err := self.GetUserWithHashes(ctx, principal, username) if err != nil { return nil, err } + // Clear the hashes result.PasswordHash = nil result.PasswordSalt = nil return result, nil } -func getUserWithHashes( +func (self *UserManager) GetUserWithHashes( ctx context.Context, principal, username string) (*api_proto.VelociraptorUser, error) { @@ -43,10 +56,15 @@ func getUserWithHashes( return nil, err } - user_manager := services.GetUserManager() - // Hold on to the error until after ACL check - user, user_err := user_manager.GetUserWithHashes(ctx, username) + user, user_err := self.storage.GetUserWithHashes(ctx, username) + if user_err != nil { + return nil, user_err + } + + // Fill in the org memberships for the user record using the org + // manager. + self.normalizeOrgList(ctx, user) // A user can always get their own user record regarless of // permissions. @@ -64,6 +82,8 @@ func getUserWithHashes( return nil, acls.PermissionDenied } + // Filter the org list according to the principal's permissions + // and visibility rules. allowed_full := false returned_orgs := []*api_proto.OrgRecord{} @@ -106,6 +126,7 @@ func getUserWithHashes( Name: user.Name, } + // We are allowed to return a filtered list of org memberships. if allowed_full { user_record.Orgs = returned_orgs } diff --git a/users/get_test.go b/services/users/get_test.go similarity index 69% rename from users/get_test.go rename to services/users/get_test.go index 382c45c7a5a..694ebf793c7 100644 --- a/users/get_test.go +++ b/services/users/get_test.go @@ -5,7 +5,7 @@ import ( "github.com/sebdah/goldie" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/vtesting/assert" ) @@ -15,35 +15,36 @@ func (self *UserManagerTestSuite) TestGetUsers() { golden := ordereddict.NewDict() // Can a simple user get their own record? Should get the full record. - user_record, err := users.GetUser(self.Ctx, "UserO1", "UserO1") + users_manager := services.GetUserManager() + user_record, err := users_manager.GetUser(self.Ctx, "UserO1", "UserO1") assert.NoError(self.T(), err) golden.Set("UserO1 GetUser UserO1", user_record) // Can they get a different user in their own orgs? Only the name! - user_record, err = users.GetUser(self.Ctx, "UserO1", "AdminO1") + user_record, err = users_manager.GetUser(self.Ctx, "UserO1", "AdminO1") assert.NoError(self.T(), err) golden.Set("UserO1 GetUser AdminO1", user_record) // Can they get a different user in another org? Nope. - user_record, err = users.GetUser(self.Ctx, "UserO1", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "UserO1", "UserO2") assert.Error(self.T(), err, "PermissionDenied") // An admin can get any user in their org - full record - user_record, err = users.GetUser(self.Ctx, "AdminO1", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "UserO1") assert.NoError(self.T(), err) golden.Set("AdminO1 GetUser UserO1", user_record) // But an admin in one org can not see users in another org - user_record, err = users.GetUser(self.Ctx, "AdminO1", "AdminO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "AdminO2") assert.Error(self.T(), err, "PermissionDenied") // Getting an invalid user gives PermissionDenied - user_record, err = users.GetUser(self.Ctx, "AdminO1", "InvalidUsername") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "InvalidUsername") assert.Error(self.T(), err, "PermissionDenied") // An org admin can see all users - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO1") assert.NoError(self.T(), err) golden.Set("OrgAdmin GetUser UserO1", user_record) @@ -51,23 +52,23 @@ func (self *UserManagerTestSuite) TestGetUsers() { admin_policy := &acl_proto.ApiClientACL{ Roles: []string{"administrator"}, } - err = users.AddUserToOrg(self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg(self.Ctx, services.UseExistingUser, "OrgAdmin", "AdminO1", []string{"O1", "O2"}, admin_policy) assert.NoError(self.T(), err) - err = users.AddUserToOrg(self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg(self.Ctx, services.UseExistingUser, "OrgAdmin", "UserO1", []string{"O1", "O2"}, admin_policy) assert.NoError(self.T(), err) // When AdminO2 looks at UserO1 they can only see the O2 // membership because AdminO2 does not have access to O1. - user_record, err = users.GetUser(self.Ctx, "AdminO2", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO2", "UserO1") assert.NoError(self.T(), err) golden.Set("AdminO2 GetUser UserO1 - filtered Org list", user_record) // When AdminO1 looks at UserO1 they can see all the Org // memberships because AdminO1 does have access to O1 and O2. - user_record, err = users.GetUser(self.Ctx, "AdminO1", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "UserO1") assert.NoError(self.T(), err) golden.Set("AdminO1 GetUser UserO1 - can see full Org list", user_record) diff --git a/services/users/grpc.go b/services/users/grpc.go index 3641eb5673b..d74e17c9c6f 100644 --- a/services/users/grpc.go +++ b/services/users/grpc.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc/peer" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/constants" crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/logging" @@ -17,23 +18,37 @@ import ( ) func (self UserManager) GetUserFromContext(ctx context.Context) ( - *api_proto.VelociraptorUser, *config_proto.Config, error) { + user_record *api_proto.VelociraptorUser, org_config_obj *config_proto.Config, err error) { grpc_user_info := GetGRPCUserInfo(self.config_obj, ctx, self.ca_pool) - user_record, err := self.GetUser(ctx, grpc_user_info.Name) - if err != nil { - return nil, nil, err + + // This is not a real user but represents the grpc gateway + // connection - it is always allowed. + if grpc_user_info.Name == constants.PinnedServerName || + (self.config_obj.API != nil && + self.config_obj.API.PinnedGwName == grpc_user_info.Name) { + user_record = &api_proto.VelociraptorUser{ + Name: grpc_user_info.Name, + } + + } else { + user_record, err = self.storage.GetUserWithHashes(ctx, grpc_user_info.Name) + if err != nil { + return nil, nil, err + } } user_record.CurrentOrg = grpc_user_info.CurrentOrg + user_record.PasswordSalt = nil + user_record.PasswordHash = nil - // Fetch the appropriate config file fro the org manager. + // Fetch the appropriate config file from the org manager. org_manager, err := services.GetOrgManager() if err != nil { return nil, nil, err } - org_config_obj, err := org_manager.GetOrgConfig(user_record.CurrentOrg) + org_config_obj, err = org_manager.GetOrgConfig(user_record.CurrentOrg) return user_record, org_config_obj, err } diff --git a/services/users/list.go b/services/users/list.go new file mode 100644 index 00000000000..c01f62e7263 --- /dev/null +++ b/services/users/list.go @@ -0,0 +1,127 @@ +package users + +import ( + "context" + + "www.velocidex.com/golang/velociraptor/acls" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" +) + +// List all the users visible to this principal. +// - If the principal is an ORG_ADMIN they can see all users +// - If the user is SERVER_ADMIN they will see the full user records. +// - Otherwise list all users belonging to the orgs in which the user +// has at least read access. Only user names will be shown. +func (self *UserManager) ListUsers( + ctx context.Context, + principal string, orgs []string) ([]*api_proto.VelociraptorUser, error) { + + // ORG_ADMINs can see everything + principal_is_org_admin, _ := services.CheckAccess(self.config_obj, + principal, acls.ORG_ADMIN) + + org_manager, err := services.GetOrgManager() + if err != nil { + return nil, err + } + + users, err := self.storage.ListAllUsers(ctx) + if err != nil { + return nil, err + } + + // Filter the users according to the access + result := make([]*api_proto.VelociraptorUser, 0, len(users)) + type org_info struct { + // If the principal is only a user in this org they can only + // see partial records. + allowed bool + + // If the principal is an admin in this org they can see the + // full record. + allowed_full bool + + org_config_obj *config_proto.Config + org_name string + } + + // A plan of which orgs will be visible to the principal. + plan := make(map[string]*org_info) + for _, org := range org_manager.ListOrgs() { + // Caller is only interested in these orgs. + if len(orgs) > 0 && !utils.OrgIdInList(org.Id, orgs) { + continue + } + + info := &org_info{ + org_name: org.Name, + } + + info.org_config_obj, err = org_manager.GetOrgConfig(org.Id) + if err != nil { + continue + } + + if principal_is_org_admin { + info.allowed_full = true + plan[org.Id] = info + continue + } + + // Server admin can see the full record. + allowed, _ := services.CheckAccess(info.org_config_obj, + principal, acls.SERVER_ADMIN) + if allowed { + info.allowed_full = true + plan[org.Id] = info + continue + } + + // A user in that org can see a partial record. + allowed, _ = services.CheckAccess(info.org_config_obj, + principal, acls.READ_RESULTS) + if allowed { + info.allowed = true + plan[org.Id] = info + continue + } + } + + // Filtering the user list according to the following criteria: + // The principal can see that org + // The user has at least read permission in the org. + for _, user := range users { + user.Orgs = nil + + // This is the record we will return. + user_record := &api_proto.VelociraptorUser{ + Name: user.Name, + } + + for org_id, org_info := range plan { + // Is the user in this org? + user_in_org, err := services.CheckAccess(org_info.org_config_obj, + user.Name, acls.READ_RESULTS) + if err != nil || !user_in_org { + continue + } + + user_record.Orgs = append(user_record.Orgs, &api_proto.OrgRecord{ + Id: utils.NormalizedOrgId(org_id), + Name: org_info.org_name, + }) + } + + // No orgs are visible - hide the user + if len(user_record.Orgs) == 0 { + continue + } + + result = append(result, user_record) + } + + return result, nil +} diff --git a/users/list_test.go b/services/users/list_test.go similarity index 68% rename from users/list_test.go rename to services/users/list_test.go index 61a6a814188..68e63ef0d56 100644 --- a/users/list_test.go +++ b/services/users/list_test.go @@ -5,7 +5,7 @@ import ( "github.com/sebdah/goldie" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/vtesting/assert" ) @@ -16,26 +16,27 @@ func (self *UserManagerTestSuite) TestListUsers() { // OrgAdmin is an org admin at the root org so should see all // users and all their orgs. - user_list, err := users.ListUsers( - self.Ctx, "OrgAdmin", users.LIST_ALL_ORGS) + users_manager := services.GetUserManager() + user_list, err := users_manager.ListUsers( + self.Ctx, "OrgAdmin", services.LIST_ALL_ORGS) assert.NoError(self.T(), err) golden.Set("OrgAdmin ListUsers", user_list) // Only list in one org - user_list, err = users.ListUsers(self.Ctx, "OrgAdmin", []string{"O1"}) + user_list, err = users_manager.ListUsers(self.Ctx, "OrgAdmin", []string{"O1"}) assert.NoError(self.T(), err) golden.Set("OrgAdmin ListUsers in O1", user_list) // AdminO1 can only see users in O1 - user_list, err = users.ListUsers( - self.Ctx, "AdminO1", users.LIST_ALL_ORGS) + user_list, err = users_manager.ListUsers( + self.Ctx, "AdminO1", services.LIST_ALL_ORGS) assert.NoError(self.T(), err) golden.Set("AdminO1 ListUsers", user_list) // UserO1 can only see user names in O1 - user_list, err = users.ListUsers( - self.Ctx, "UserO1", users.LIST_ALL_ORGS) + user_list, err = users_manager.ListUsers( + self.Ctx, "UserO1", services.LIST_ALL_ORGS) assert.NoError(self.T(), err) golden.Set("UserO1 ListUsers", user_list) @@ -44,20 +45,20 @@ func (self *UserManagerTestSuite) TestListUsers() { admin_policy := &acl_proto.ApiClientACL{ Roles: []string{"administrator"}, } - err = users.AddUserToOrg(self.Ctx, users.UseExistingUser, + err = users_manager.AddUserToOrg(self.Ctx, services.UseExistingUser, "OrgAdmin", "AdminO1", []string{"O1", "O2"}, admin_policy) assert.NoError(self.T(), err) // List users as AdminO2. AdminO2 can see AdminO1 in their org, // but the org list visible should only mention O2. - user_list, err = users.ListUsers( - self.Ctx, "AdminO2", users.LIST_ALL_ORGS) + user_list, err = users_manager.ListUsers( + self.Ctx, "AdminO2", services.LIST_ALL_ORGS) assert.NoError(self.T(), err) golden.Set("AdminO2 ListUsers - Filtered AdminO1 Orgs", user_list) // But the actual record for AdminO1 still contains both - user_list, err = users.ListUsers( - self.Ctx, "AdminO1", users.LIST_ALL_ORGS) + user_list, err = users_manager.ListUsers( + self.Ctx, "AdminO1", services.LIST_ALL_ORGS) assert.NoError(self.T(), err) golden.Set("AdminO1 ListUsers - Includes both AdminO1 Orgs", user_list) diff --git a/users/orgs.go b/services/users/orgs.go similarity index 65% rename from users/orgs.go rename to services/users/orgs.go index 40f13f046cf..b98b5ded681 100644 --- a/users/orgs.go +++ b/services/users/orgs.go @@ -9,9 +9,8 @@ import ( ) // List all the orgs the user can see. -func GetOrgs( - ctx context.Context, - principal string) []*api_proto.OrgRecord { +func (self *UserManager) GetOrgs( + ctx context.Context, principal string) []*api_proto.OrgRecord { org_manager, err := services.GetOrgManager() if err != nil { @@ -23,7 +22,8 @@ func GetOrgs( return nil } - // ORG_ADMINs can see everything + // ORG_ADMINs can see everything so they have permissions in all + // the orgs is_superuser, _ := services.CheckAccess( root_config_obj, principal, acls.ORG_ADMIN) @@ -61,3 +61,19 @@ func GetOrgs( return result } + +// Fill in the orgs that the user has any permissions in. +func (self *UserManager) normalizeOrgList( + ctx context.Context, + user_record *api_proto.VelociraptorUser) { + orgs := self.GetOrgs(ctx, user_record.Name) + user_record.Orgs = nil + + // Fill in some information from the orgs but not everything. + for _, org_record := range orgs { + user_record.Orgs = append(user_record.Orgs, &api_proto.OrgRecord{ + Id: org_record.Id, + Name: org_record.Name, + }) + } +} diff --git a/users/orgs_test.go b/services/users/orgs_test.go similarity index 66% rename from users/orgs_test.go rename to services/users/orgs_test.go index a563483a8e8..3c926210686 100644 --- a/users/orgs_test.go +++ b/services/users/orgs_test.go @@ -4,22 +4,22 @@ import ( "github.com/Velocidex/ordereddict" "github.com/sebdah/goldie" "www.velocidex.com/golang/velociraptor/json" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" ) func (self *UserManagerTestSuite) TestListOrgs() { self.makeUsers() golden := ordereddict.NewDict() - + users_manager := services.GetUserManager() golden.Set("OrgAdmin Sees all orgs", - users.GetOrgs(self.Ctx, "OrgAdmin")) + users_manager.GetOrgs(self.Ctx, "OrgAdmin")) golden.Set("AdminO1 Sees only O1 with nonce", - users.GetOrgs(self.Ctx, "AdminO1")) + users_manager.GetOrgs(self.Ctx, "AdminO1")) golden.Set("UserO1 Sees only O1 but does not see nonce", - users.GetOrgs(self.Ctx, "UserO1")) + users_manager.GetOrgs(self.Ctx, "UserO1")) goldie.Assert(self.T(), "TestListOrgs", json.MustMarshalIndent(golden)) } diff --git a/users/set_user.go b/services/users/set_user.go similarity index 87% rename from users/set_user.go rename to services/users/set_user.go index 41d9cc5be00..7d9f7308f91 100644 --- a/users/set_user.go +++ b/services/users/set_user.go @@ -12,7 +12,6 @@ import ( "www.velocidex.com/golang/velociraptor/acls" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/services" ) @@ -24,14 +23,15 @@ var ( // A user may update their own password. // A ServerAdmin in any of the orgs the user belongs to can update their password. // An OrgAdmin can update everyone's password -func SetUserPassword( +func (self *UserManager) SetUserPassword( ctx context.Context, config_obj *config_proto.Config, principal, username string, password, current_org string) error { - if isNameReserved(username) { - return NameReservedError + err := validateUsername(config_obj, username) + if err != nil { + return err } org_manager, err := services.GetOrgManager() @@ -47,7 +47,7 @@ func SetUserPassword( user_manager := services.GetUserManager() // Hold on to the error until after ACL check - user_record, user_err := user_manager.GetUser(ctx, username) + user_record, user_err := user_manager.GetUser(ctx, principal, username) // Update the password if needed. if password != "" { @@ -143,25 +143,15 @@ func verifyPassword(self *api_proto.VelociraptorUser, password string) bool { return subtle.ConstantTimeCompare(hash[:], self.PasswordHash) == 1 } -func VerifyPassword( +func (self *UserManager) VerifyPassword( ctx context.Context, principal, username string, password string) (bool, error) { - user_record, err := getUserWithHashes(ctx, principal, username) + user_record, err := self.GetUserWithHashes(ctx, principal, username) if err != nil { return false, err } return verifyPassword(user_record, password), nil } - -// Store this special name from being added - This principal is used -// internally by the server to bypass the ACL system when needed. -func isNameReserved(username string) bool { - switch username { - case constants.PinnedServerName: - return true - } - return false -} diff --git a/users/set_user_test.go b/services/users/set_user_test.go similarity index 75% rename from users/set_user_test.go rename to services/users/set_user_test.go index ee88613acd3..47c0b922b69 100644 --- a/users/set_user_test.go +++ b/services/users/set_user_test.go @@ -1,7 +1,7 @@ package users_test import ( - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/vtesting/assert" ) @@ -9,28 +9,29 @@ func (self *UserManagerTestSuite) TestSetUserPassword() { self.makeUsers() // Can a user update their password? - err := users.SetUserPassword( + users_manager := services.GetUserManager() + err := users_manager.SetUserPassword( self.Ctx, self.ConfigObj, "UserO1", "UserO1", "MyPassword", "") assert.NoError(self.T(), err) // Verify the password - ok, err := users.VerifyPassword( + ok, err := users_manager.VerifyPassword( self.Ctx, "UserO1", "UserO1", "MyPassword") assert.NoError(self.T(), err) assert.True(self.T(), ok) // Can a user update an admin's password? - err = users.SetUserPassword( + err = users_manager.SetUserPassword( self.Ctx, self.ConfigObj, "UserO1", "AdminO1", "MyPassword", "") assert.Error(self.T(), err, "PermissionDenied") // Can an admin update a user's password? - err = users.SetUserPassword( + err = users_manager.SetUserPassword( self.Ctx, self.ConfigObj, "AdminO1", "UserO1", "MyPassword", "") assert.NoError(self.T(), err) // Can a user set current org to a different org? - err = users.SetUserPassword( + err = users_manager.SetUserPassword( self.Ctx, self.ConfigObj, "UserO1", "UserO1", "", "O2") assert.Error(self.T(), err, "PermissionDenied") } diff --git a/services/users/storage.go b/services/users/storage.go new file mode 100644 index 00000000000..d9f18c038a9 --- /dev/null +++ b/services/users/storage.go @@ -0,0 +1,504 @@ +package users + +import ( + "context" + "errors" + "os" + "sync" + "time" + + "github.com/Velocidex/ordereddict" + "github.com/Velocidex/ttlcache/v2" + "google.golang.org/protobuf/proto" + api_proto "www.velocidex.com/golang/velociraptor/api/proto" + config_proto "www.velocidex.com/golang/velociraptor/config/proto" + "www.velocidex.com/golang/velociraptor/datastore" + "www.velocidex.com/golang/velociraptor/paths" + "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" +) + +// Responsible for storing the User records +type IUserStorageManager interface { + GetUserWithHashes(ctx context.Context, username string) ( + *api_proto.VelociraptorUser, error) + + SetUser(ctx context.Context, user_record *api_proto.VelociraptorUser) error + + ListAllUsers(ctx context.Context) ([]*api_proto.VelociraptorUser, error) + + GetUserOptions(ctx context.Context, username string) ( + *api_proto.SetGUIOptionsRequest, error) + + SetUserOptions(ctx context.Context, + username string, options *api_proto.SetGUIOptionsRequest) error + + // Favourites are stored per org. + GetFavorites( + ctx context.Context, + org_config_obj *config_proto.Config, + principal, fav_type string) (*api_proto.Favorites, error) + + DeleteUser(ctx context.Context, username string) error +} + +// The NullStorage Manager is used for tools and clients. In this +// configuration there are no users and none of the user based VQL +// plugins will work. +type NullStorageManager struct{} + +func (self *NullStorageManager) GetUserWithHashes(ctx context.Context, username string) ( + *api_proto.VelociraptorUser, error) { + return nil, errors.New("Not Found") +} + +func (self *NullStorageManager) SetUser(ctx context.Context, + user_record *api_proto.VelociraptorUser) error { + return errors.New("Not Implemented") +} + +func (self *NullStorageManager) ListAllUsers( + ctx context.Context) ([]*api_proto.VelociraptorUser, error) { + return nil, errors.New("Not Implemented") +} + +func (self *NullStorageManager) GetUserOptions(ctx context.Context, username string) ( + *api_proto.SetGUIOptionsRequest, error) { + return nil, errors.New("Not Implemented") +} + +func (self *NullStorageManager) SetUserOptions(ctx context.Context, + username string, options *api_proto.SetGUIOptionsRequest) error { + return errors.New("Not Implemented") +} + +func (self *NullStorageManager) DeleteUser(ctx context.Context, username string) error { + return errors.New("Not Implemented") +} + +func (self *NullStorageManager) GetFavorites( + ctx context.Context, org_config_obj *config_proto.Config, + principal, fav_type string) (*api_proto.Favorites, error) { + return nil, errors.New("Not Implemented") +} + +/* + The User Manager is responsible for coordinating access to user + records. +*/ + +// The object that is cached in the LRU +type _CachedUserObject struct { + user_record *api_proto.VelociraptorUser + gui_options *api_proto.SetGUIOptionsRequest +} + +type UserStorageManager struct { + mu sync.Mutex + + config_obj *config_proto.Config + + lru *ttlcache.Cache + id int64 +} + +func (self *UserStorageManager) GetUserWithHashes(ctx context.Context, username string) ( + *api_proto.VelociraptorUser, error) { + self.mu.Lock() + defer self.mu.Unlock() + + if username == "" { + return nil, errors.New("Must set a username") + } + + var cache *_CachedUserObject + var ok bool + + // Check the LRU for a cache if it is there + cache_any, err := self.lru.Get(username) + if err == nil { + cache, ok = cache_any.(*_CachedUserObject) + if ok && cache.user_record != nil { + return proto.Clone(cache.user_record).(*api_proto.VelociraptorUser), nil + } + } + + // Otherwise add a new cache + if cache == nil { + cache = &_CachedUserObject{} + } + + err = validateUsername(self.config_obj, username) + if err != nil { + return nil, err + } + + db, err := datastore.GetDB(self.config_obj) + if err != nil { + return nil, err + } + + user_record := &api_proto.VelociraptorUser{} + err = db.GetSubject(self.config_obj, + paths.UserPathManager{Name: username}.Path(), user_record) + if errors.Is(err, os.ErrNotExist) || user_record.Name == "" { + return nil, services.UserNotFoundError + } + + if err != nil { + return nil, err + } + + // Add the record to the lru + cache.user_record = proto.Clone(user_record).(*api_proto.VelociraptorUser) + self.lru.Set(username, cache) + + return user_record, nil +} + +// Update the record in the LRU +func (self *UserStorageManager) SetUser( + ctx context.Context, user_record *api_proto.VelociraptorUser) error { + self.mu.Lock() + defer self.mu.Unlock() + + if user_record.Name == "" { + return errors.New("Must set a username") + } + + err := validateUsername(self.config_obj, user_record.Name) + if err != nil { + return err + } + + var cache *_CachedUserObject + + // Check the LRU for a cache if it is there + cache_any, err := self.lru.Get(user_record.Name) + if err == nil { + cache, _ = cache_any.(*_CachedUserObject) + } + if cache == nil { + cache = &_CachedUserObject{} + } + cache.user_record = proto.Clone(user_record).(*api_proto.VelociraptorUser) + + db, err := datastore.GetDB(self.config_obj) + if err != nil { + return err + } + + err = db.SetSubject(self.config_obj, + paths.UserPathManager{Name: user_record.Name}.Path(), + user_record) + if err != nil { + return err + } + + self.lru.Set(user_record.Name, cache) + return self.notifyChanges(ctx, user_record.Name) +} + +// Advertise the changes. This will force all minions to flush their +// caches. +func (self *UserStorageManager) notifyChanges( + ctx context.Context, username string) error { + journal_service, err := services.GetJournal(self.config_obj) + if err != nil { + return err + } + + return journal_service.PushRowsToArtifact(ctx, self.config_obj, + []*ordereddict.Dict{ + ordereddict.NewDict().Set("id", self.id).Set("username", username), + }, + "Server.Internal.UserManager", "server", "") +} + +func (self *UserStorageManager) ListAllUsers( + ctx context.Context) ([]*api_proto.VelociraptorUser, error) { + db, err := datastore.GetDB(self.config_obj) + if err != nil { + return nil, err + } + + children, err := db.ListChildren(self.config_obj, paths.USERS_ROOT) + if err != nil { + return nil, err + } + + result := make([]*api_proto.VelociraptorUser, 0, len(children)) + for _, child := range children { + if child.IsDir() { + continue + } + + username := child.Base() + user_record, err := self.GetUserWithHashes(ctx, username) + if err == nil { + user_record.PasswordHash = nil + user_record.PasswordSalt = nil + user_record.Orgs = nil + result = append(result, user_record) + } + } + + return result, nil +} + +func (self *UserStorageManager) SetUserOptions(ctx context.Context, + username string, options *api_proto.SetGUIOptionsRequest) error { + + self.mu.Lock() + defer self.mu.Unlock() + + var cache *_CachedUserObject + + // Check the LRU for a cache if it is there + cache_any, err := self.lru.Get(username) + if err == nil { + cache, _ = cache_any.(*_CachedUserObject) + } + if cache == nil { + cache = &_CachedUserObject{} + } + cache.gui_options = proto.Clone(options).(*api_proto.SetGUIOptionsRequest) + + path_manager := paths.UserPathManager{Name: username} + db, err := datastore.GetDB(self.config_obj) + if err != nil { + return err + } + + // Merge the old options with the new options + old_options, err := self.getUserOptions(ctx, username) + if err != nil { + old_options = &api_proto.SetGUIOptionsRequest{} + } + + // For now we do not allow the user to set the links in their + // profile. + old_options.Links = nil + + if options.Lang != "" { + old_options.Lang = options.Lang + } + + if options.Theme != "" { + old_options.Theme = options.Theme + } + + if options.Timezone != "" { + old_options.Timezone = options.Timezone + } + + if options.Org != "" { + old_options.Org = options.Org + } + + if options.Options != "" { + old_options.Options = options.Options + } + + old_options.DefaultPassword = options.DefaultPassword + old_options.DefaultDownloadsLock = options.DefaultDownloadsLock + + err = db.SetSubject(self.config_obj, path_manager.GUIOptions(), old_options) + if err != nil { + return err + } + + // Update the LRU + self.lru.Set(username, cache) + return self.notifyChanges(ctx, username) +} + +func (self *UserStorageManager) GetUserOptions(ctx context.Context, username string) ( + *api_proto.SetGUIOptionsRequest, error) { + self.mu.Lock() + defer self.mu.Unlock() + + return self.getUserOptions(ctx, username) +} + +func (self *UserStorageManager) getUserOptions(ctx context.Context, username string) ( + *api_proto.SetGUIOptionsRequest, error) { + + var cache *_CachedUserObject + var ok bool + + // Check the LRU for a cache if it is there + cache_any, err := self.lru.Get(username) + if err == nil { + cache, ok = cache_any.(*_CachedUserObject) + if ok && cache.gui_options != nil { + return proto.Clone(cache.gui_options).(*api_proto.SetGUIOptionsRequest), nil + } + } + + // Otherwise add a new cache + if cache == nil { + cache = &_CachedUserObject{} + } + + path_manager := paths.UserPathManager{Name: username} + db, err := datastore.GetDB(self.config_obj) + if err != nil { + return nil, err + } + + options := &api_proto.SetGUIOptionsRequest{} + err = db.GetSubject(self.config_obj, path_manager.GUIOptions(), options) + if options.Options == "" { + options.Options = default_user_options + } + + // Add any links in the config file to the user's preferences. + if self.config_obj.GUI != nil { + options.Links = MergeGUILinks(options.Links, self.config_obj.GUI.Links) + } + + // Add the defaults. + options.Links = MergeGUILinks(options.Links, DefaultLinks) + + // NOTE: It is possible for a user to disable one of the default + // targets by simply adding an entry with disabled: true - we will + // not override the configured link from the default and it will + // be ignored. + + defaults := &config_proto.Defaults{} + if self.config_obj.Defaults != nil { + defaults = self.config_obj.Defaults + } + + // Deprecated - moved to customizations + options.DisableServerEvents = defaults.DisableServerEvents + options.DisableQuarantineButton = defaults.DisableQuarantineButton + + if options.Customizations == nil { + options.Customizations = &api_proto.GUICustomizations{} + } + options.Customizations.HuntExpiryHours = defaults.HuntExpiryHours + options.Customizations.DisableServerEvents = defaults.DisableServerEvents + options.Customizations.DisableQuarantineButton = defaults.DisableQuarantineButton + + // Specify a default theme if specified in the config file. + if options.Theme == "" { + options.Theme = defaults.DefaultTheme + } + + // Add the record to the lru + cache.gui_options = proto.Clone(options).(*api_proto.SetGUIOptionsRequest) + self.lru.Set(username, cache) + + return options, nil +} + +func (self *UserStorageManager) GetFavorites( + ctx context.Context, + config_obj *config_proto.Config, + principal, fav_type string) (*api_proto.Favorites, error) { + result := &api_proto.Favorites{} + path_manager := paths.NewUserPathManager(principal) + + db, err := datastore.GetDB(config_obj) + if err != nil { + return nil, err + } + + components := path_manager.FavoriteDir(fav_type) + children, err := db.ListChildren(config_obj, components) + if err != nil { + return nil, err + } + + for _, child := range children { + if child.IsDir() { + continue + } + + fav := &api_proto.Favorite{} + err = db.GetSubject(config_obj, + path_manager.Favorites(child.Base(), fav_type), fav) + if err == nil { + result.Items = append(result.Items, fav) + } + } + + return result, nil +} + +func (self *UserStorageManager) DeleteUser(ctx context.Context, username string) error { + self.mu.Lock() + defer self.mu.Unlock() + + db, err := datastore.GetDB(self.config_obj) + if err != nil { + return err + } + + // No more orgs for this user, Just remove the user completely + user_path_manager := paths.NewUserPathManager(username) + err = db.DeleteSubject(self.config_obj, user_path_manager.Path()) + if err != nil { + return err + } + + self.lru.Remove(username) + return self.notifyChanges(ctx, username) +} + +func NewUserStorageManager( + ctx context.Context, + wg *sync.WaitGroup, + config_obj *config_proto.Config) (*UserStorageManager, error) { + result := &UserStorageManager{ + config_obj: config_obj, + lru: ttlcache.NewCache(), + id: utils.GetGUID(), + } + + result.lru.SetCacheSizeLimit(1000) + result.lru.SetTTL(time.Minute) + + journal_service, err := services.GetJournal(config_obj) + if err != nil { + return nil, err + } + events, cancel := journal_service.Watch(ctx, + "Server.Internal.UserManager", "UserManagerService") + + // Invalidate the ttl when a username is changed. + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + for { + select { + case <-ctx.Done(): + return + + case event, ok := <-events: + if !ok { + return + } + + // Skip our own messages + id, pres := event.GetInt64("id") + if !pres || id == result.id { + continue + } + + username, pres := event.GetString("username") + if pres { + result.mu.Lock() + result.lru.Remove(username) + result.mu.Unlock() + } + } + } + }() + + return result, nil +} diff --git a/services/users/users.go b/services/users/users.go index 99023420d66..70485af968d 100644 --- a/services/users/users.go +++ b/services/users/users.go @@ -21,21 +21,16 @@ import ( "context" "crypto/rand" "crypto/sha256" - "crypto/subtle" "crypto/x509" - "errors" "fmt" - "os" "regexp" "sync" api_proto "www.velocidex.com/golang/velociraptor/api/proto" config_proto "www.velocidex.com/golang/velociraptor/config/proto" - datastore "www.velocidex.com/golang/velociraptor/datastore" + "www.velocidex.com/golang/velociraptor/constants" "www.velocidex.com/golang/velociraptor/logging" - "www.velocidex.com/golang/velociraptor/paths" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" ) const ( @@ -99,6 +94,8 @@ type UserManager struct { // This is the root org's config since there is only a single user // manager. config_obj *config_proto.Config + + storage IUserStorageManager } func validateUsername(config_obj *config_proto.Config, name string) error { @@ -108,16 +105,16 @@ func validateUsername(config_obj *config_proto.Config, name string) error { if config_obj.API != nil && config_obj.API.PinnedGwName == name { - return fmt.Errorf("Unacceptable username %v", name) + return fmt.Errorf("Username is reserved: %v", name) } if config_obj.Client != nil && config_obj.Client.PinnedServerName == name { - return fmt.Errorf("Unacceptable username %v", name) + return fmt.Errorf("Username is reserved: %v", name) } - if name == "GRPC_GW" || name == "VelociraptorServer" { - return fmt.Errorf("Unacceptable username %v", name) + if name == constants.PinnedGwName || name == constants.PinnedServerName { + return fmt.Errorf("Username is reserved: %v", name) } return nil @@ -144,230 +141,21 @@ func SetPassword(user_record *api_proto.VelociraptorUser, password string) { user_record.Locked = false } -func VerifyPassword(self *api_proto.VelociraptorUser, password string) bool { - hash := sha256.Sum256(append(self.PasswordSalt, []byte(password)...)) - return subtle.ConstantTimeCompare(hash[:], self.PasswordHash) == 1 -} - func (self UserManager) SetUser( ctx context.Context, user_record *api_proto.VelociraptorUser) error { - if user_record.Name == "" { - return errors.New("Must set a username") - } - - err := validateUsername(self.config_obj, user_record.Name) - if err != nil { - return err - } - - db, err := datastore.GetDB(self.config_obj) - if err != nil { - return err - } - - return db.SetSubject(self.config_obj, - paths.UserPathManager{Name: user_record.Name}.Path(), - user_record) -} - -func (self UserManager) ListUsers( - ctx context.Context) ([]*api_proto.VelociraptorUser, error) { - db, err := datastore.GetDB(self.config_obj) - if err != nil { - return nil, err - } - - children, err := db.ListChildren(self.config_obj, paths.USERS_ROOT) - if err != nil { - return nil, err - } - - result := make([]*api_proto.VelociraptorUser, 0, len(children)) - for _, child := range children { - if child.IsDir() { - continue - } - - username := child.Base() - user_record, err := self.GetUser(ctx, username) - if err == nil { - result = append(result, user_record) - } - } - - return result, nil -} - -// Fill in the orgs the user has any permissions in. -func normalizeOrgList( - ctx context.Context, - user_record *api_proto.VelociraptorUser) { - orgs := users.GetOrgs(ctx, user_record.Name) - user_record.Orgs = nil - - // Fill in some information from the orgs but not everything. - for _, org_record := range orgs { - user_record.Orgs = append(user_record.Orgs, &api_proto.OrgRecord{ - Id: org_record.Id, - Name: org_record.Name, - }) - } -} - -// Returns the user record after stripping sensitive information like -// password hashes. -func (self UserManager) GetUser(ctx context.Context, username string) ( - *api_proto.VelociraptorUser, error) { - - // For the server name we dont have a real user record, we make a - // hard coded user record instead. - if username == self.config_obj.Client.PinnedServerName { - return &api_proto.VelociraptorUser{ - Name: username, - }, nil - } - - result, err := self.GetUserWithHashes(ctx, username) - if err != nil { - return nil, err - } - - // Do not divulge the password and hashes. - result.PasswordHash = nil - result.PasswordSalt = nil - - return result, nil -} - -// Return the user record with hashes - only used in Basic Auth. -func (self UserManager) GetUserWithHashes(ctx context.Context, username string) ( - *api_proto.VelociraptorUser, error) { - if username == "" { - return nil, errors.New("Must set a username") - } - - err := validateUsername(self.config_obj, username) - if err != nil { - return nil, err - } - - db, err := datastore.GetDB(self.config_obj) - if err != nil { - return nil, err - } - - user_record := &api_proto.VelociraptorUser{} - err = db.GetSubject(self.config_obj, - paths.UserPathManager{Name: username}.Path(), user_record) - if errors.Is(err, os.ErrNotExist) || user_record.Name == "" { - return nil, services.UserNotFoundError - } - - if err != nil { - return nil, err - } - - normalizeOrgList(ctx, user_record) - return user_record, nil + return self.storage.SetUser(ctx, user_record) } func (self UserManager) SetUserOptions(ctx context.Context, username string, options *api_proto.SetGUIOptionsRequest) error { - - path_manager := paths.UserPathManager{Name: username} - db, err := datastore.GetDB(self.config_obj) - if err != nil { - return err - } - - // Merge the old options with the new options - old_options, err := self.GetUserOptions(ctx, username) - if err != nil { - old_options = &api_proto.SetGUIOptionsRequest{} - } - - // For now we do not allow the user to set the links in their - // profile. - old_options.Links = nil - - if options.Lang != "" { - old_options.Lang = options.Lang - } - - if options.Theme != "" { - old_options.Theme = options.Theme - } - - if options.Timezone != "" { - old_options.Timezone = options.Timezone - } - - if options.Org != "" { - old_options.Org = options.Org - } - - if options.Options != "" { - old_options.Options = options.Options - } - - old_options.DefaultPassword = options.DefaultPassword - old_options.DefaultDownloadsLock = options.DefaultDownloadsLock - - return db.SetSubject(self.config_obj, path_manager.GUIOptions(), old_options) + return self.storage.SetUserOptions(ctx, username, options) } func (self UserManager) GetUserOptions(ctx context.Context, username string) ( *api_proto.SetGUIOptionsRequest, error) { - - path_manager := paths.UserPathManager{Name: username} - db, err := datastore.GetDB(self.config_obj) - if err != nil { - return nil, err - } - - options := &api_proto.SetGUIOptionsRequest{} - err = db.GetSubject(self.config_obj, path_manager.GUIOptions(), options) - if options.Options == "" { - options.Options = default_user_options - } - - // Add any links in the config file to the user's preferences. - if self.config_obj.GUI != nil { - options.Links = MergeGUILinks(options.Links, self.config_obj.GUI.Links) - } - - // Add the defaults. - options.Links = MergeGUILinks(options.Links, DefaultLinks) - - // NOTE: It is possible for a user to disable one of the default - // targets by simply adding an entry with disabled: true - we will - // not override the configured link from the default and it will - // be ignored. - - defaults := &config_proto.Defaults{} - if self.config_obj.Defaults != nil { - defaults = self.config_obj.Defaults - } - - // Deprecated - moved to customizations - options.DisableServerEvents = defaults.DisableServerEvents - options.DisableQuarantineButton = defaults.DisableQuarantineButton - - if options.Customizations == nil { - options.Customizations = &api_proto.GUICustomizations{} - } - options.Customizations.HuntExpiryHours = defaults.HuntExpiryHours - options.Customizations.DisableServerEvents = defaults.DisableServerEvents - options.Customizations.DisableQuarantineButton = defaults.DisableQuarantineButton - - // Specify a default theme if specified in the config file. - if options.Theme == "" { - options.Theme = defaults.DefaultTheme - } - - return options, nil + return self.storage.GetUserOptions(ctx, username) } func StartUserManager( @@ -383,9 +171,15 @@ func StartUserManager( CA_Pool.AppendCertsFromPEM([]byte(config_obj.Client.CaCertificate)) } + storage, err := NewUserStorageManager(ctx, wg, config_obj) + if err != nil { + return err + } + service := &UserManager{ ca_pool: CA_Pool, config_obj: config_obj, + storage: storage, } services.RegisterUserManager(service) @@ -397,6 +191,7 @@ func init() { service := &UserManager{ ca_pool: x509.NewCertPool(), config_obj: &config_proto.Config{}, + storage: &NullStorageManager{}, } services.RegisterUserManager(service) } diff --git a/users/users_test.go b/services/users/users_test.go similarity index 79% rename from users/users_test.go rename to services/users/users_test.go index 0e214d98afe..f1670aaf4c5 100644 --- a/users/users_test.go +++ b/services/users/users_test.go @@ -11,7 +11,6 @@ import ( "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/services" "www.velocidex.com/golang/velociraptor/services/orgs" - "www.velocidex.com/golang/velociraptor/users" "www.velocidex.com/golang/velociraptor/vtesting/assert" ) @@ -20,10 +19,15 @@ type UserManagerTestSuite struct { } func (self *UserManagerTestSuite) SetupTest() { + self.ConfigObj = self.TestSuite.LoadConfig() + self.ConfigObj.Services.JournalService = true + self.TestSuite.SetupTest() self.LoadArtifacts(`name: Server.Audit.Logs type: SERVER_EVENT +`, `name: Server.Internal.UserManager +type: INTERNAL `) } @@ -70,32 +74,32 @@ func (self *UserManagerTestSuite) TestMakeUsers() { self.makeUsers() golden := ordereddict.NewDict() - - user_record, err := users.GetUser(self.Ctx, "OrgAdmin", "OrgAdmin") + users_manager := services.GetUserManager() + user_record, err := users_manager.GetUser(self.Ctx, "OrgAdmin", "OrgAdmin") assert.NoError(self.T(), err) golden.Set("OrgAdmin OrgAdmin", user_record) - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO1") assert.NoError(self.T(), err) golden.Set("OrgAdmin UserO1", user_record) - user_record, err = users.GetUser(self.Ctx, "AdminO1", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "UserO1") assert.NoError(self.T(), err) golden.Set("AdminO1 UserO1", user_record) - user_record, err = users.GetUser(self.Ctx, "AdminO2", "UserO1") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO2", "UserO1") assert.ErrorContains(self.T(), err, "PermissionDenied") golden.Set("AdminO2 UserO1", err.Error()) - user_record, err = users.GetUser(self.Ctx, "OrgAdmin", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "OrgAdmin", "UserO2") assert.NoError(self.T(), err) golden.Set("OrgAdmin UserO2", user_record) - user_record, err = users.GetUser(self.Ctx, "AdminO2", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO2", "UserO2") assert.NoError(self.T(), err) golden.Set("AdminO2 UserO2", user_record) - user_record, err = users.GetUser(self.Ctx, "AdminO1", "UserO2") + user_record, err = users_manager.GetUser(self.Ctx, "AdminO1", "UserO2") assert.ErrorContains(self.T(), err, "PermissionDenied") golden.Set("AdminO1 UserO2", err.Error()) diff --git a/users/delete.go b/users/delete.go deleted file mode 100644 index 4cde96cbafb..00000000000 --- a/users/delete.go +++ /dev/null @@ -1,94 +0,0 @@ -package users - -import ( - "context" - - "github.com/pkg/errors" - "www.velocidex.com/golang/velociraptor/acls" - acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" - api_proto "www.velocidex.com/golang/velociraptor/api/proto" - "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/utils" -) - -// Removes a user from an org -func DeleteUser( - ctx context.Context, - principal, username string, - orgs []string) error { - - if isNameReserved(username) { - return NameReservedError - } - - user_manager := services.GetUserManager() - - // Hold on to the error until after ACL check - user_record, user_err := user_manager.GetUserWithHashes(ctx, username) - - org_manager, err := services.GetOrgManager() - if err != nil { - return err - } - - root_config_obj, err := org_manager.GetOrgConfig(services.ROOT_ORG_ID) - if err != nil { - return err - } - - principal_is_org_admin, _ := services.CheckAccess( - root_config_obj, principal, acls.ORG_ADMIN) - - if user_err != nil { - if principal_is_org_admin { - return user_err - } - return errors.Errorf("Error %v: User %v is not org admin", - acls.PermissionDenied, principal) - } - - remaining_orgs := []*api_proto.OrgRecord{} - // Empty policy - no permissions. - policy := &acl_proto.ApiClientACL{} - - for _, user_org := range user_record.Orgs { - org_config_obj, err := org_manager.GetOrgConfig(user_org.Id) - if err != nil { - remaining_orgs = append(remaining_orgs, user_org) - continue - } - - // Skip orgs that are not specified. - if len(orgs) > 0 && !utils.OrgIdInList(user_org.Id, orgs) { - remaining_orgs = append(remaining_orgs, user_org) - continue - } - - // Further checks if the principal is not ORG_ADMIN - if !principal_is_org_admin { - ok, _ := services.CheckAccess( - org_config_obj, principal, acls.SERVER_ADMIN) - if !ok { - // If the user is not server admin on this org they - // may not remove the user from this org - remaining_orgs = append(remaining_orgs, user_org) - continue - } - } - - // Reset the user's ACLs in this org. - err = services.SetPolicy(org_config_obj, username, policy) - if err != nil { - return err - } - } - - if len(remaining_orgs) > 0 { - // Update the user's record - user_record.Orgs = remaining_orgs - return user_manager.SetUser(ctx, user_record) - } - - // No more orgs for this user, Just remove the user completely - return user_manager.DeleteUser(ctx, root_config_obj, username) -} diff --git a/users/list.go b/users/list.go deleted file mode 100644 index e06977e3eb7..00000000000 --- a/users/list.go +++ /dev/null @@ -1,116 +0,0 @@ -package users - -import ( - "context" - - "www.velocidex.com/golang/velociraptor/acls" - api_proto "www.velocidex.com/golang/velociraptor/api/proto" - "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/utils" -) - -var ( - LIST_ALL_ORGS []string = nil -) - -// List all the users visible to this principal. -// - If the principal is an ORG_ADMIN they can see all users -// - If the user is SERVER_ADMIN they will see the full user records. -// - Otherwise list all users belonging to the orgs in which the user -// has at least read access. Only user names will be shown. -func ListUsers( - ctx context.Context, - principal string, orgs []string) ([]*api_proto.VelociraptorUser, error) { - - org_manager, err := services.GetOrgManager() - if err != nil { - return nil, err - } - - root_config_obj, err := org_manager.GetOrgConfig(services.ROOT_ORG_ID) - if err != nil { - return nil, err - } - - user_manager := services.GetUserManager() - users, err := user_manager.ListUsers(ctx) - if err != nil { - return nil, err - } - - // Filter the users according to the access - result := make([]*api_proto.VelociraptorUser, 0, len(users)) - type org_info struct { - hidden bool - allowed bool - allowed_full bool - } - seen := make(map[string]org_info) - - for _, user := range users { - // This is the record we will return. - user_record := &api_proto.VelociraptorUser{ - Name: user.Name, - } - - allowed_full := false - returned_orgs := []*api_proto.OrgRecord{} - - for _, user_org := range user.Orgs { - info, pres := seen[user_org.Id] - if !pres { - if len(orgs) > 0 && !utils.OrgIdInList(user_org.Id, orgs) { - info.hidden = true - } else { - org_config_obj, err := org_manager.GetOrgConfig(user_org.Id) - if err != nil { - continue - } - - // ORG_ADMINs can see everything - info.allowed_full, _ = services.CheckAccess( - root_config_obj, principal, acls.ORG_ADMIN) - - // Otherwise the user is an admin in their org - if !info.allowed_full { - info.allowed_full, _ = services.CheckAccess(org_config_obj, - principal, acls.SERVER_ADMIN) - } - - // If they just have reader access in their org - // they only see the name. - if !info.allowed_full { - info.allowed, _ = services.CheckAccess(org_config_obj, - principal, acls.READ_RESULTS) - } - } - seen[user_org.Id] = info - } - - if info.hidden { - continue - } - - // If we have full access, copy the entire record. - if info.allowed_full { - allowed_full = true - - // If we have only read access only copy the name. - } else if !info.allowed { - continue - } - - returned_orgs = append(returned_orgs, user_org) - } - - if len(returned_orgs) > 0 { - if allowed_full { - user_record.Orgs = returned_orgs - } - - result = append(result, user_record) - } - } - - return result, nil -} diff --git a/utils/counter.go b/utils/counter.go index 4226c959536..5bec305d170 100644 --- a/utils/counter.go +++ b/utils/counter.go @@ -8,9 +8,10 @@ import ( ) var ( - idx uint64 + idx uint64 = uint64(GetGUID() >> 4) ) +// Get unique ID func GetId() uint64 { return atomic.AddUint64(&idx, 1) } diff --git a/vql/server/orgs/delete.go b/vql/server/orgs/delete.go index 9b16eb57d15..efd999cab88 100644 --- a/vql/server/orgs/delete.go +++ b/vql/server/orgs/delete.go @@ -50,13 +50,13 @@ func (self OrgDeleteFunction) Call( return vfilter.Null{} } - err = org_manager.DeleteOrg(ctx, arg.OrgId) + principal := vql_subsystem.GetPrincipal(scope) + err = org_manager.DeleteOrg(ctx, principal, arg.OrgId) if err != nil { scope.Log("org_delete: %s", err) return vfilter.Null{} } - principal := vql_subsystem.GetPrincipal(scope) services.LogAudit(ctx, config_obj, principal, "org_delete", ordereddict.NewDict(). diff --git a/vql/server/orgs/orgs.go b/vql/server/orgs/orgs.go index e41d93438f4..5dededc42cc 100644 --- a/vql/server/orgs/orgs.go +++ b/vql/server/orgs/orgs.go @@ -7,7 +7,6 @@ import ( "github.com/Velocidex/yaml/v2" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" ) @@ -23,6 +22,7 @@ func (self OrgsPlugin) Call( go func() { defer close(output_chan) + user_manager := services.GetUserManager() org_manager, err := services.GetOrgManager() if err != nil { scope.Log("orgs: %v", err) @@ -31,7 +31,7 @@ func (self OrgsPlugin) Call( // ACLs are checked by the users module principal := vql_subsystem.GetPrincipal(scope) - for _, org_record := range users.GetOrgs(ctx, principal) { + for _, org_record := range user_manager.GetOrgs(ctx, principal) { org_config_obj, err := org_manager.GetOrgConfig(org_record.Id) if err != nil { continue diff --git a/vql/server/users/create.go b/vql/server/users/create.go index 5b17a05b4b0..123d12aae12 100644 --- a/vql/server/users/create.go +++ b/vql/server/users/create.go @@ -6,7 +6,6 @@ import ( "github.com/Velocidex/ordereddict" acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" @@ -50,7 +49,8 @@ func (self UserCreateFunction) Call( Roles: arg.Roles, } - err = users.AddUserToOrg(ctx, users.AddNewUser, + users_manager := services.GetUserManager() + err = users_manager.AddUserToOrg(ctx, services.AddNewUser, principal, arg.Username, arg.OrgIds, policy) if err != nil { scope.Log("user_create: %s", err) @@ -66,7 +66,7 @@ func (self UserCreateFunction) Call( if arg.Password != "" { // Write the user record. - err = users.SetUserPassword( + err = users_manager.SetUserPassword( ctx, org_config_obj, principal, arg.Username, arg.Password, "") if err != nil { scope.Log("user_create: %s", err) diff --git a/vql/server/users/delete.go b/vql/server/users/delete.go index 052ce2d3b0f..978f97a9157 100644 --- a/vql/server/users/delete.go +++ b/vql/server/users/delete.go @@ -5,7 +5,6 @@ import ( "github.com/Velocidex/ordereddict" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" @@ -45,7 +44,8 @@ func (self UserDeleteFunction) Call( if arg.ReallyDoIt { principal := vql_subsystem.GetPrincipal(scope) - err = users.DeleteUser(ctx, principal, arg.Username, orgs) + users_manager := services.GetUserManager() + err = users_manager.DeleteUser(ctx, principal, arg.Username, orgs) if err != nil { scope.Log("user_delete: %s", err) return vfilter.Null{} diff --git a/vql/server/users/get.go b/vql/server/users/get.go index a317f23cba0..98fce7fa512 100644 --- a/vql/server/users/get.go +++ b/vql/server/users/get.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Velocidex/ordereddict" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" @@ -37,7 +37,8 @@ func (self UserFunction) Call( } principal := vql_subsystem.GetPrincipal(scope) - user_details, err := users.GetUser(ctx, principal, arg.Username) + users_manager := services.GetUserManager() + user_details, err := users_manager.GetUser(ctx, principal, arg.Username) if err != nil { scope.Log("user: %s", err) return vfilter.Null{} diff --git a/vql/server/users/grant.go b/vql/server/users/grant.go index da9c8ea28b3..bcfd665c6ce 100644 --- a/vql/server/users/grant.go +++ b/vql/server/users/grant.go @@ -7,7 +7,6 @@ import ( acl_proto "www.velocidex.com/golang/velociraptor/acls/proto" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" "www.velocidex.com/golang/velociraptor/utils" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" @@ -67,7 +66,7 @@ func (self GrantFunction) Call( policy.Roles = utils.DeduplicateStringSlice(append(policy.Roles, arg.Roles...)) principal := vql_subsystem.GetPrincipal(scope) - err = users.GrantUserToOrg(ctx, principal, arg.Username, orgs, policy) + err = services.GrantUserToOrg(ctx, principal, arg.Username, orgs, policy) if err != nil { scope.Log("user_grant: %s", err) return vfilter.Null{} diff --git a/vql/server/users/password.go b/vql/server/users/password.go index cc24ddbb161..158ecdd703d 100644 --- a/vql/server/users/password.go +++ b/vql/server/users/password.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Velocidex/ordereddict" - "www.velocidex.com/golang/velociraptor/users" + "www.velocidex.com/golang/velociraptor/services" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" @@ -41,7 +41,8 @@ func (self SetPasswordFunction) Call( return vfilter.Null{} } - err = users.SetUserPassword(ctx, config_obj, principal, arg.Username, + users_manager := services.GetUserManager() + err = users_manager.SetUserPassword(ctx, config_obj, principal, arg.Username, arg.Password, "") if err != nil { scope.Log("passwd: %v", err) diff --git a/vql/server/users/users.go b/vql/server/users/users.go index a48791c22c7..a1cd2409218 100644 --- a/vql/server/users/users.go +++ b/vql/server/users/users.go @@ -8,7 +8,6 @@ import ( api_proto "www.velocidex.com/golang/velociraptor/api/proto" "www.velocidex.com/golang/velociraptor/json" "www.velocidex.com/golang/velociraptor/services" - "www.velocidex.com/golang/velociraptor/users" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" "www.velocidex.com/golang/vfilter/arg_parser" @@ -47,13 +46,14 @@ func (self UsersPlugin) Call( return } - orgs := users.LIST_ALL_ORGS + orgs := services.LIST_ALL_ORGS if !arg.AllOrgs { // Only list the current org. orgs = []string{config_obj.OrgId} } - user_list, err := users.ListUsers(ctx, principal, orgs) + users_manager := services.GetUserManager() + user_list, err := users_manager.ListUsers(ctx, principal, orgs) if err != nil { scope.Log("users: %v", err) return