mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: add GetAdmins method to retrieve admin users from the plugin
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
ae5ecc6ac3
commit
605792d7f8
@ -25,4 +25,11 @@ type UsersService interface {
|
||||
// Returns a slice of users the plugin can access, or an empty slice if none configured.
|
||||
//nd:hostfunc
|
||||
GetUsers(ctx context.Context) ([]User, error)
|
||||
|
||||
// GetAdmins returns only admin users the plugin has been granted access to.
|
||||
// This is a convenience method that filters GetUsers results to include only admins.
|
||||
//
|
||||
// Returns a slice of admin users the plugin can access, or an empty slice if none.
|
||||
//nd:hostfunc
|
||||
GetAdmins(ctx context.Context) ([]User, error)
|
||||
}
|
||||
|
||||
@ -15,11 +15,18 @@ type UsersGetUsersResponse struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// UsersGetAdminsResponse is the response type for Users.GetAdmins.
|
||||
type UsersGetAdminsResponse struct {
|
||||
Result []User `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterUsersHostFunctions registers Users service host functions.
|
||||
// The returned host functions should be added to the plugin's configuration.
|
||||
func RegisterUsersHostFunctions(service UsersService) []extism.HostFunction {
|
||||
return []extism.HostFunction{
|
||||
newUsersGetUsersHostFunction(service),
|
||||
newUsersGetAdminsHostFunction(service),
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +53,29 @@ func newUsersGetUsersHostFunction(service UsersService) extism.HostFunction {
|
||||
)
|
||||
}
|
||||
|
||||
func newUsersGetAdminsHostFunction(service UsersService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"users_getadmins",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
|
||||
// Call the service method
|
||||
result, svcErr := service.GetAdmins(ctx)
|
||||
if svcErr != nil {
|
||||
usersWriteError(p, stack, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := UsersGetAdminsResponse{
|
||||
Result: result,
|
||||
}
|
||||
usersWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
// usersWriteResponse writes a JSON response to plugin memory.
|
||||
func usersWriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
|
||||
respBytes, err := json.Marshal(resp)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/plugins/host"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
)
|
||||
|
||||
type usersServiceImpl struct {
|
||||
@ -49,4 +50,15 @@ func (s *usersServiceImpl) GetUsers(ctx context.Context) ([]host.User, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *usersServiceImpl) GetAdmins(ctx context.Context) ([]host.User, error) {
|
||||
users, err := s.GetUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return slice.Filter(users, func(u host.User) bool {
|
||||
return u.IsAdmin
|
||||
}), nil
|
||||
}
|
||||
|
||||
var _ host.UsersService = (*usersServiceImpl)(nil)
|
||||
|
||||
@ -141,6 +141,86 @@ var _ = Describe("UsersService", Ordered, func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAdmins", func() {
|
||||
var mockUserRepo *tests.MockedUserRepo
|
||||
|
||||
BeforeEach(func() {
|
||||
mockUserRepo = ds.User(ctx).(*tests.MockedUserRepo)
|
||||
// Add test users - alice is admin, bob and charlie are not
|
||||
_ = mockUserRepo.Put(&model.User{
|
||||
ID: "user1",
|
||||
UserName: "alice",
|
||||
Name: "Alice Admin",
|
||||
IsAdmin: true,
|
||||
})
|
||||
_ = mockUserRepo.Put(&model.User{
|
||||
ID: "user2",
|
||||
UserName: "bob",
|
||||
Name: "Bob User",
|
||||
IsAdmin: false,
|
||||
})
|
||||
_ = mockUserRepo.Put(&model.User{
|
||||
ID: "user3",
|
||||
UserName: "charlie",
|
||||
Name: "Charlie User",
|
||||
IsAdmin: false,
|
||||
})
|
||||
})
|
||||
|
||||
Context("with allUsers=true", func() {
|
||||
BeforeEach(func() {
|
||||
service = newUsersService(ds, nil, true)
|
||||
})
|
||||
|
||||
It("should return only admin users", func() {
|
||||
admins, err := service.GetAdmins(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(admins).To(HaveLen(1))
|
||||
Expect(admins[0].UserName).To(Equal("alice"))
|
||||
Expect(admins[0].IsAdmin).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("with specific allowed users including admin", func() {
|
||||
BeforeEach(func() {
|
||||
// Allow access to user1 (admin) and user2 (non-admin)
|
||||
service = newUsersService(ds, []string{"user1", "user2"}, false)
|
||||
})
|
||||
|
||||
It("should return only admin users from allowed list", func() {
|
||||
admins, err := service.GetAdmins(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(admins).To(HaveLen(1))
|
||||
Expect(admins[0].UserName).To(Equal("alice"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("with specific allowed users excluding admin", func() {
|
||||
BeforeEach(func() {
|
||||
// Only allow access to non-admin users
|
||||
service = newUsersService(ds, []string{"user2", "user3"}, false)
|
||||
})
|
||||
|
||||
It("should return empty when no admins in allowed list", func() {
|
||||
admins, err := service.GetAdmins(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(admins).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when datastore returns error", func() {
|
||||
BeforeEach(func() {
|
||||
mockUserRepo.Error = model.ErrNotFound
|
||||
service = newUsersService(ds, nil, true)
|
||||
})
|
||||
|
||||
It("should propagate the error", func() {
|
||||
_, err := service.GetAdmins(ctx)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("UsersService Integration", Ordered, func() {
|
||||
@ -215,6 +295,16 @@ var _ = Describe("UsersService Integration", Ordered, func() {
|
||||
Expect(bob.IsAdmin).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetAdmins Operations via Plugin", func() {
|
||||
It("should get only admin users when allUsers is true", func() {
|
||||
output, err := callTestUsersPlugin(GinkgoT().Context(), manager, testUsersInput{Operation: "get_admins"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Users).To(HaveLen(1))
|
||||
Expect(output.Users[0].UserName).To(Equal("alice"))
|
||||
Expect(output.Users[0].IsAdmin).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("UsersService Integration with Specific Users", Ordered, func() {
|
||||
@ -240,6 +330,34 @@ var _ = Describe("UsersService Integration with Specific Users", Ordered, func()
|
||||
Expect(userNames).To(ContainElements("alice", "charlie"))
|
||||
Expect(userNames).ToNot(ContainElement("bob"))
|
||||
})
|
||||
|
||||
It("should only return admin users from allowed list via GetAdmins", func() {
|
||||
output, err := callTestUsersPlugin(GinkgoT().Context(), manager, testUsersInput{Operation: "get_admins"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Only alice (user1) is admin, charlie (user3) is not
|
||||
Expect(output.Users).To(HaveLen(1))
|
||||
Expect(output.Users[0].UserName).To(Equal("alice"))
|
||||
Expect(output.Users[0].IsAdmin).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("UsersService Integration GetAdmins with No Admins", Ordered, func() {
|
||||
var manager *Manager
|
||||
|
||||
BeforeAll(func() {
|
||||
var cleanup func()
|
||||
// Only allow user2 (bob) and user3 (charlie), both non-admins
|
||||
manager, cleanup = setupUsersIntegrationManager(false, `["user2", "user3"]`)
|
||||
DeferCleanup(cleanup)
|
||||
})
|
||||
|
||||
Describe("GetAdmins with no admin users in allowed list", func() {
|
||||
It("should return empty when no admins in allowed list", func() {
|
||||
output, err := callTestUsersPlugin(GinkgoT().Context(), manager, testUsersInput{Operation: "get_admins"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Users).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -28,11 +28,21 @@ type User struct {
|
||||
//go:wasmimport extism:host/user users_getusers
|
||||
func users_getusers(uint64) uint64
|
||||
|
||||
// users_getadmins is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user users_getadmins
|
||||
func users_getadmins(uint64) uint64
|
||||
|
||||
type usersGetUsersResponse struct {
|
||||
Result []User `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type usersGetAdminsResponse struct {
|
||||
Result []User `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// UsersGetUsers calls the users_getusers host function.
|
||||
// GetUsers returns all users the plugin has been granted access to.
|
||||
// Only minimal user information (userName, name, isAdmin) is returned.
|
||||
@ -64,3 +74,34 @@ func UsersGetUsers() ([]User, error) {
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
// UsersGetAdmins calls the users_getadmins host function.
|
||||
// GetAdmins returns only admin users the plugin has been granted access to.
|
||||
// This is a convenience method that filters GetUsers results to include only admins.
|
||||
//
|
||||
// Returns a slice of admin users the plugin can access, or an empty slice if none.
|
||||
func UsersGetAdmins() ([]User, error) {
|
||||
// No parameters - allocate empty JSON object
|
||||
reqMem := pdk.AllocateBytes([]byte("{}"))
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := users_getadmins(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response usersGetAdminsResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return nil, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
@ -43,3 +43,18 @@ func (m *mockUsersService) GetUsers() ([]User, error) {
|
||||
func UsersGetUsers() ([]User, error) {
|
||||
return UsersMock.GetUsers()
|
||||
}
|
||||
|
||||
// GetAdmins is the mock method for UsersGetAdmins.
|
||||
func (m *mockUsersService) GetAdmins() ([]User, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]User), args.Error(1)
|
||||
}
|
||||
|
||||
// UsersGetAdmins delegates to the mock instance.
|
||||
// GetAdmins returns only admin users the plugin has been granted access to.
|
||||
// This is a convenience method that filters GetUsers results to include only admins.
|
||||
//
|
||||
// Returns a slice of admin users the plugin can access, or an empty slice if none.
|
||||
func UsersGetAdmins() ([]User, error) {
|
||||
return UsersMock.GetAdmins()
|
||||
}
|
||||
|
||||
@ -25,6 +25,12 @@ def _users_getusers(offset: int) -> int:
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "users_getadmins")
|
||||
def _users_getadmins(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def users_get_users() -> Any:
|
||||
"""GetUsers returns all users the plugin has been granted access to.
|
||||
Only minimal user information (userName, name, isAdmin) is returned.
|
||||
@ -48,3 +54,27 @@ Returns a slice of users the plugin can access, or an empty slice if none config
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("result", None)
|
||||
|
||||
|
||||
def users_get_admins() -> Any:
|
||||
"""GetAdmins returns only admin users the plugin has been granted access to.
|
||||
This is a convenience method that filters GetUsers results to include only admins.
|
||||
|
||||
Returns a slice of admin users the plugin can access, or an empty slice if none.
|
||||
|
||||
Returns:
|
||||
Any: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request_bytes = b"{}"
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _users_getadmins(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("result", None)
|
||||
|
||||
@ -25,9 +25,19 @@ struct UsersGetUsersResponse {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UsersGetAdminsResponse {
|
||||
#[serde(default)]
|
||||
result: Vec<User>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[host_fn]
|
||||
extern "ExtismHost" {
|
||||
fn users_getusers(input: Json<serde_json::Value>) -> Json<UsersGetUsersResponse>;
|
||||
fn users_getadmins(input: Json<serde_json::Value>) -> Json<UsersGetAdminsResponse>;
|
||||
}
|
||||
|
||||
/// GetUsers returns all users the plugin has been granted access to.
|
||||
@ -52,3 +62,25 @@ pub fn get_users() -> Result<Vec<User>, Error> {
|
||||
|
||||
Ok(response.0.result)
|
||||
}
|
||||
|
||||
/// GetAdmins returns only admin users the plugin has been granted access to.
|
||||
/// This is a convenience method that filters GetUsers results to include only admins.
|
||||
///
|
||||
/// Returns a slice of admin users the plugin can access, or an empty slice if none.
|
||||
///
|
||||
/// # Returns
|
||||
/// The result value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_admins() -> Result<Vec<User>, Error> {
|
||||
let response = unsafe {
|
||||
users_getadmins(Json(serde_json::json!({})))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok(response.0.result)
|
||||
}
|
||||
|
||||
12
plugins/testdata/test-users/main.go
vendored
12
plugins/testdata/test-users/main.go
vendored
@ -10,7 +10,7 @@ import (
|
||||
|
||||
// TestUsersInput is the input for nd_test_users callback.
|
||||
type TestUsersInput struct {
|
||||
Operation string `json:"operation"` // "get_users"
|
||||
Operation string `json:"operation"` // "get_users", "get_admins"
|
||||
}
|
||||
|
||||
// TestUsersOutput is the output from nd_test_users callback.
|
||||
@ -41,6 +41,16 @@ func ndTestUsers() int32 {
|
||||
pdk.OutputJSON(TestUsersOutput{Users: users})
|
||||
return 0
|
||||
|
||||
case "get_admins":
|
||||
admins, err := host.UsersGetAdmins()
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestUsersOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
pdk.OutputJSON(TestUsersOutput{Users: admins})
|
||||
return 0
|
||||
|
||||
default:
|
||||
errStr := "unknown operation: " + input.Operation
|
||||
pdk.OutputJSON(TestUsersOutput{Error: &errStr})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user