diff --git a/docs/03-candella-app.md b/docs/03-candella-app.md index a71984a..c45c78d 100644 --- a/docs/03-candella-app.md +++ b/docs/03-candella-app.md @@ -51,10 +51,11 @@ The manifest file contains information about your app, as well as what the app n - `description`: A summary of what your app does. - `license`: The license your app falls under as an SPDX expression. - `permissions`: A list of strings containing what permissions your app requires: - - `file_system`: Requires access to the Candella "file system" - - `notifications`: Requires access to NotificationKit to send notifications - - `system_events`: Requires access to System Events - - `virtual_platform`: Requires access to the MeteorVM platform + - `file_system`: Requires access to the Candella "file system" + - `notifications`: Requires access to NotificationKit to send notifications + - `system_events`: Requires access to System Events + - `manage_users`: Requires access to the accounts service to manage users + - `virtual_platform`: Requires access to the MeteorVM platform The following is an example manifest: @@ -106,6 +107,9 @@ Returns the path for a given icon size. Apps can also store data for the currently logged-in user on the system; this data can be used to save preferences or other data that is necessary for app functions. App storage is handled by the `AppStorage` class and can be accessed for your app via `CAApplication.data`. +!!! warning "Declare file system permissions" + Apps that utilize app storage must include the `file_system` permission in their app manifest in the `permissions` field. Apps will not be able to access app storage if this permission isn't declared or if the user has not granted the app permission to do so. + There are four methods in `AppStorage` to help read and write data accordingly: - `AppStorage.read(field)` will fetch the value for a field or return `None` if no value for the field was found. diff --git a/docs/06-multiuser.md b/docs/05-multiuser.md similarity index 100% rename from docs/06-multiuser.md rename to docs/05-multiuser.md diff --git a/docs/06-accounts-service.md b/docs/06-accounts-service.md new file mode 100644 index 0000000..dd2415c --- /dev/null +++ b/docs/06-accounts-service.md @@ -0,0 +1,54 @@ +# Accounts Service + +The **Accounts Service** for Candella extends the functionality of the Multiuser framework and adds support for managing users in a way that lets other services handle this functionality. This service defines a single class, `CAAccountsService`, to manage users efficiently. + +!!! note "Backwards Compatibility with AliceOS" + To maintain backwards compatibility with AliceOS, the username of the currently logged-in user is set in AliceOS's `persistent.playername`. This field is commonly used by services and apps for the current player, and it also defines the player's name in games like _Doki Doki Literature Club!_. + +## Viewing information about the userspace + +Currently, there are two static methods available to get information about the userspace: + +- `CAAccountsService.get_logged_in_user()` returns a `CAUser` object that contains the information about the currently logged-in user. +- `CAAccountsService.get_all_users()` returns a list of `CAUser` objects that contain information about all users known to Candella in the userspace. + +## Managing users + +While accessing user information is static and does not require instancing, `CAAccountsService` should be instanced in the service or app that requests these features to manage data more efficiently. Including an instance in your service or app ensures that no other services are using that app's copy, and it helps reduce variables in Ren'Py's store. + +!!! warning "Declare user permissions" + For an app or service to manage users, the `manage_users` permission must be enabled for that service or app. This can be defined in the `permissions` field in an app's manifest and the `requisites` field in a service's manifest, respectively. + +To instantiate an instance of the account manager, you'll need to pass in the service or app instance with the `manage_users` permission: + +```py +class SampleCoreService(CACoreService): + def __init__(self): + CACoreService.__init__(self) + self._accounts = CAAccountsService(self) +``` + +### `add_user(self, username, pretty_name=None)` + +Creates the user file for a given user and adds it to the user list. + +**Arguments** + +- username (str): The username for the new user. +- pretty_name (str): The display name for the new user. + +### `change_current_user(self, username)` + +Change the currently logged-in user. + +**Arguments** + +- username (str): The username of the user to switch to. + +### `remove_user(self, username)` + +Removes the specified user. + +**Arguments** + +- username (str): The username of the user to remove. \ No newline at end of file diff --git a/game/System/CoreServices/Accounts.aoscservice/AccountsService.rpy b/game/System/CoreServices/Accounts.aoscservice/AccountsService.rpy index b783d29..e098bff 100644 --- a/game/System/CoreServices/Accounts.aoscservice/AccountsService.rpy +++ b/game/System/CoreServices/Accounts.aoscservice/AccountsService.rpy @@ -9,10 +9,13 @@ init offset = -10 init python: import os - + class CAAccountsUserNotFoundError(Exception): """The specified user was not found.""" - + + class CAAccountsPermissionError(Exception): + """The core service or app does not have permission to use CAAccountsService.""" + class CAAccountsService(ASCoreServiceRepresentative): bundleName = "Accounts Service" bundleId = "dev.unscriptedvn.candella.accounts-service" @@ -22,44 +25,52 @@ init python: bundleDescription = """\ Manage multiple user accounts via the Multiuser framework. """ - + _accounts_dir = config.savedir + "/.causerland" _users_list = [] - - def __init__(self): + + def __init__(self, appobj): ASCoreServiceRepresentative.__init__(self, AS_CORESERVICES_DIR + "Accounts.aoscservice/") self._users_list = CAAccountsService.get_all_users() - + + # Disable access to this service on Candella apps and services that don't request this behavior. + if isinstance(appobj, CAApplication): + if "manage_users" not in appobj.permissions: + raise CAAccountsPermissionError(appobj.__class__.__name__) + elif isinstance(appobj, CACoreService): + if "manage_users" not in appobj.requisites: + raise CAAccountsPermissionError(appobj.__class__.__name__) + @staticmethod def get_logged_in_user(): """Returns the user information for the currently logged in user.""" _user = CAUserData(persistent.playername) return CAUser(_user._data["name"], _user._data["prettyName"]) - + @staticmethod def get_all_users(): """Returns a list of user objects.""" users = [] - + if not os.path.isdir(config.savedir + "/.causerland"): return [] - + for name in os.listdir(config.savedir + "/.causerland/"): if name.startswith("."): continue users += [CAAccountsService._get_user(name)] return users - + @staticmethod def _get_user(username): """Returns the user information for a specified user. - + Arguments: username (str): The username of the user to get. - + Returns: user (CAUser): The user information. - + Raises: If the user doesn't exist in the userland folder, a CAAccountsUserNotFoundError is raised. """ @@ -67,25 +78,25 @@ init python: raise CAAccountsUserNotFoundError(username) _data = CAUserData(username) return CAUser(_data._data["name"], _data._data["prettyName"]) - + def add_user(self, username, pretty_name=None): """Creates the user file for a given user and adds it to the user list. - + Arguments: username (str): The username for the new user. pretty_name (str): The display name for the new user. """ CAUserData._create_user_file(username, pretty_name=pretty_name) _users_list = CAUser(username, pretty_name) - + def change_current_user(self, username): """Change the currently logged-in user.""" persistent.playername = username # renpy.reload() - + def remove_user(self, username): """Removes the specified user. - + Arguments: username (str): The username of the user to remove. """ @@ -96,4 +107,4 @@ init python: continue self._users_list.remove(user) except: - pass \ No newline at end of file + pass diff --git a/game/System/CoreServices/Caberto.aoscservice/CabertoDE.rpy b/game/System/CoreServices/Caberto.aoscservice/CabertoDE.rpy index 837f28a..22b12e5 100644 --- a/game/System/CoreServices/Caberto.aoscservice/CabertoDE.rpy +++ b/game/System/CoreServices/Caberto.aoscservice/CabertoDE.rpy @@ -14,7 +14,6 @@ init python: from time import gmtime, strftime class CabertoShell(CACoreService): - _acct_mgr = CAAccountsService() _wallpaper = AS_LIBRARY_DIR + "Desktop Pictures/Candella.png" _dock = [] _drawer_open = False @@ -23,6 +22,8 @@ init python: def __init__(self): CACoreService.__init__(self, AS_CORESERVICES_DIR + "Caberto.aoscservice/") + self._acct_mgr = CAAccountsService(self) + self.settings = ServiceStorage(self) if not self.settings.read("apps_list"): diff --git a/game/System/CoreServices/Caberto.aoscservice/manifest.json b/game/System/CoreServices/Caberto.aoscservice/manifest.json index c15b118..cc39c95 100644 --- a/game/System/CoreServices/Caberto.aoscservice/manifest.json +++ b/game/System/CoreServices/Caberto.aoscservice/manifest.json @@ -5,5 +5,5 @@ "version": "1.0.0", "description": "A desktop shell for Candella, inspired by Lomiri.", "license": "Proprietary", - "requisites": ["file_system"] + "requisites": ["file_system", "manage_users"] } diff --git a/game/System/Frameworks/AppKit.aosframework/Permissions/CAPermissions.rpy b/game/System/Frameworks/AppKit.aosframework/Permissions/CAPermissions.rpy index 6b904fc..e8d785a 100644 --- a/game/System/Frameworks/AppKit.aosframework/Permissions/CAPermissions.rpy +++ b/game/System/Frameworks/AppKit.aosframework/Permissions/CAPermissions.rpy @@ -15,23 +15,23 @@ init python: name = "Default Permission" description = "A default permission." default_state = False - + def __init__(self, key, name, description, default=False): self.key = key self.name = name self.description = description self.default = default - + def __eq__(self, other): return isistance(other, CAPermission) and self.key == other.key - + def __ne__(self, other): return not self.__eq__(other) - + # This dictionary contains the current system permissions, including the default AliceOS permissions. Apps that # use CAApplication instead of ASAppRepresentative can leverage these permission objects by specifying the # permissions needed for the app in the 'permissions' field of the app manifest. - + CA_PERMISSIONS = { "notifications": CAPermission( "REQ_NOTIFICATIONKIT", @@ -49,10 +49,15 @@ init python: "Control System Events", "System events include login, shutdown, or user switching. This can be configured in App Manager." ), + "manage_users": CAPermission( + "REQ_USERS_MANAGEMENT", + "Manage Users", + "User management includes adding, modifying, and removing users. This can be configured in App Manager." + ), "virtual_platform": CAPermission( "REQ_METEORVM", "Run Apps in a Virtual Environment", "This app runs additional code in the Meteor VM platform. This can be configured in App Manager." ), - - } \ No newline at end of file + + }