nonebot-plugin-onebot-luckperms 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_onebot_luckperms/__init__.py +127 -0
- nonebot_plugin_onebot_luckperms/adapter/__init__.py +17 -0
- nonebot_plugin_onebot_luckperms/adapter/context.py +31 -0
- nonebot_plugin_onebot_luckperms/adapter/identity.py +64 -0
- nonebot_plugin_onebot_luckperms/adapter/permission.py +157 -0
- nonebot_plugin_onebot_luckperms/commands/__init__.py +3 -0
- nonebot_plugin_onebot_luckperms/commands/admin.py +857 -0
- nonebot_plugin_onebot_luckperms/config.py +20 -0
- nonebot_plugin_onebot_luckperms/core/__init__.py +20 -0
- nonebot_plugin_onebot_luckperms/core/cache.py +44 -0
- nonebot_plugin_onebot_luckperms/core/context_provider.py +74 -0
- nonebot_plugin_onebot_luckperms/core/engine.py +162 -0
- nonebot_plugin_onebot_luckperms/core/exceptions.py +6 -0
- nonebot_plugin_onebot_luckperms/core/models.py +123 -0
- nonebot_plugin_onebot_luckperms/core/registry.py +72 -0
- nonebot_plugin_onebot_luckperms/message.py +126 -0
- nonebot_plugin_onebot_luckperms/py.typed +0 -0
- nonebot_plugin_onebot_luckperms/storage/__init__.py +29 -0
- nonebot_plugin_onebot_luckperms/storage/memory.py +46 -0
- nonebot_plugin_onebot_luckperms/storage/protocol.py +21 -0
- nonebot_plugin_onebot_luckperms/storage/redis.py +162 -0
- nonebot_plugin_onebot_luckperms/storage/sqlite.py +189 -0
- nonebot_plugin_onebot_luckperms/webeditor/__init__.py +9 -0
- nonebot_plugin_onebot_luckperms/webeditor/bytebin.py +71 -0
- nonebot_plugin_onebot_luckperms/webeditor/manager.py +228 -0
- nonebot_plugin_onebot_luckperms/webeditor/session.py +109 -0
- nonebot_plugin_onebot_luckperms/webeditor/websocket.py +180 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/METADATA +23 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/RECORD +32 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/WHEEL +5 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/licenses/LICENSE +21 -0
- nonebot_plugin_onebot_luckperms-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from nonebot.plugin import PluginMetadata
|
|
4
|
+
|
|
5
|
+
from .core.registry import register_node
|
|
6
|
+
from .core.models import ContextSet, PermissionNode
|
|
7
|
+
from .core.context_provider import register_context_provider, ContextProvider
|
|
8
|
+
from .storage import get_store
|
|
9
|
+
from .adapter import set_resolver, require, require_any, require_all, get_context
|
|
10
|
+
from .adapter.context import LPContext
|
|
11
|
+
from .config import OBLPConfig, oblp_config
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("oblp")
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__plugin_meta__ = PluginMetadata(
|
|
18
|
+
name="nonebot-plugin-onebot-luckperms",
|
|
19
|
+
description="LuckPerms-style permission system for NoneBot2 + OneBot",
|
|
20
|
+
usage="Register nodes with register_node(), use require() in command matchers",
|
|
21
|
+
type="library",
|
|
22
|
+
homepage="https://github.com/XChen446/nonebot-plugin-onebot-luckperms",
|
|
23
|
+
config=OBLPConfig,
|
|
24
|
+
supported_adapters={"~onebot.v11"},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"register_node",
|
|
29
|
+
"require",
|
|
30
|
+
"require_any",
|
|
31
|
+
"require_all",
|
|
32
|
+
"get_context",
|
|
33
|
+
"set_resolver",
|
|
34
|
+
"ContextSet",
|
|
35
|
+
"PermissionNode",
|
|
36
|
+
"oblp_config",
|
|
37
|
+
"get_store",
|
|
38
|
+
"LPContext",
|
|
39
|
+
"register_context_provider",
|
|
40
|
+
"ContextProvider",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
_store_initialized = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _register_builtin_nodes():
|
|
47
|
+
register_node("luckperms.*", "Access to all LuckPerms commands", default=True)
|
|
48
|
+
register_node("luckperms.help", "View LuckPerms help", default=False)
|
|
49
|
+
register_node("luckperms.info", "View LuckPerms info", default=False)
|
|
50
|
+
register_node("luckperms.sync", "Reload LuckPerms data", default=False)
|
|
51
|
+
register_node("luckperms.editor", "Open LuckPerms Web Editor", default=False)
|
|
52
|
+
register_node("luckperms.applyedits", "Apply Web Editor edits", default=False)
|
|
53
|
+
register_node("luckperms.check", "Check user permissions", default=False)
|
|
54
|
+
register_node("luckperms.tree", "View permission inheritance tree", default=False)
|
|
55
|
+
register_node("luckperms.user.info", "View user details", default=False)
|
|
56
|
+
register_node("luckperms.user.permission", "Manage user permissions", default=False)
|
|
57
|
+
register_node("luckperms.user.parent", "Manage user group membership", default=False)
|
|
58
|
+
register_node("luckperms.user.promote", "Promote user", default=False)
|
|
59
|
+
register_node("luckperms.user.demote", "Demote user", default=False)
|
|
60
|
+
register_node("luckperms.user.clear", "Clear user permissions", default=False)
|
|
61
|
+
register_node("luckperms.user.clone", "Clone user", default=False)
|
|
62
|
+
register_node("luckperms.user.create", "Create user", default=False)
|
|
63
|
+
register_node("luckperms.user.delete", "Delete user", default=False)
|
|
64
|
+
register_node("luckperms.user.list", "List all users", default=False)
|
|
65
|
+
register_node("luckperms.group.info", "View group details", default=False)
|
|
66
|
+
register_node("luckperms.group.permission", "Manage group permissions", default=False)
|
|
67
|
+
register_node("luckperms.group.parent", "Manage group inheritance", default=False)
|
|
68
|
+
register_node("luckperms.group.setweight", "Set group weight", default=False)
|
|
69
|
+
register_node("luckperms.group.setdisplayname", "Set group display name", default=False)
|
|
70
|
+
register_node("luckperms.group.clear", "Clear group permissions", default=False)
|
|
71
|
+
register_node("luckperms.group.rename", "Rename group", default=False)
|
|
72
|
+
register_node("luckperms.group.clone", "Clone group", default=False)
|
|
73
|
+
register_node("luckperms.group.listmembers", "List group members", default=False)
|
|
74
|
+
register_node("luckperms.group.create", "Create group", default=False)
|
|
75
|
+
register_node("luckperms.group.delete", "Delete group", default=False)
|
|
76
|
+
register_node("luckperms.group.list", "List all groups", default=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_register_builtin_nodes()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def _init():
|
|
83
|
+
global _store_initialized
|
|
84
|
+
|
|
85
|
+
if _store_initialized:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
from nonebot import get_plugin_config
|
|
89
|
+
from .config import OBLPConfig
|
|
90
|
+
from .storage import init_store
|
|
91
|
+
from .adapter.identity import set_resolver as _set_resolver, OneBotV11Resolver
|
|
92
|
+
from .commands import register_admin_commands
|
|
93
|
+
from .message import load_messages, _export_defaults
|
|
94
|
+
|
|
95
|
+
raw_cfg = get_plugin_config(OBLPConfig)
|
|
96
|
+
|
|
97
|
+
for field in raw_cfg.model_fields:
|
|
98
|
+
setattr(oblp_config, field, getattr(raw_cfg, field))
|
|
99
|
+
|
|
100
|
+
store_type = oblp_config.store_type
|
|
101
|
+
kwargs = {}
|
|
102
|
+
if store_type == "sqlite":
|
|
103
|
+
kwargs["db_path"] = oblp_config.sqlite_path
|
|
104
|
+
elif store_type == "redis":
|
|
105
|
+
kwargs["redis_url"] = oblp_config.redis_url
|
|
106
|
+
|
|
107
|
+
store = init_store(store_type, **kwargs)
|
|
108
|
+
await store.load_all()
|
|
109
|
+
_set_resolver(OneBotV11Resolver())
|
|
110
|
+
register_admin_commands()
|
|
111
|
+
|
|
112
|
+
await load_messages(oblp_config.message_file)
|
|
113
|
+
try:
|
|
114
|
+
await _export_defaults(oblp_config.message_file)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
_store_initialized = True
|
|
119
|
+
logger.info(f"OBLP initialized (store: {store_type})")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
from nonebot import get_driver
|
|
124
|
+
driver = get_driver()
|
|
125
|
+
driver.on_startup(_init)
|
|
126
|
+
except (ValueError, RuntimeError, ImportError):
|
|
127
|
+
logger.debug("NoneBot not initialized, OBLP will init on first use")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .identity import Identity, IdentityResolver, OneBotV11Resolver, set_resolver
|
|
4
|
+
from .permission import require, require_any, require_all
|
|
5
|
+
from .context import get_context, LPContext
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Identity",
|
|
9
|
+
"IdentityResolver",
|
|
10
|
+
"OneBotV11Resolver",
|
|
11
|
+
"set_resolver",
|
|
12
|
+
"require",
|
|
13
|
+
"require_any",
|
|
14
|
+
"require_all",
|
|
15
|
+
"get_context",
|
|
16
|
+
"LPContext",
|
|
17
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Optional, Any
|
|
7
|
+
|
|
8
|
+
from ..core.models import User, QueryOptions, ContextSet
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class LPContext:
|
|
13
|
+
user: User
|
|
14
|
+
identity: "Identity"
|
|
15
|
+
query_options: QueryOptions
|
|
16
|
+
resolved_nodes: Dict[str, bool]
|
|
17
|
+
matched_node: Optional[str] = None
|
|
18
|
+
timestamp: float = field(default_factory=time.time)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_lp_context: contextvars.ContextVar[Optional[LPContext]] = contextvars.ContextVar(
|
|
22
|
+
"oblp_context", default=None
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_context() -> Optional[LPContext]:
|
|
27
|
+
return _lp_context.get()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_context(ctx: Optional[LPContext]):
|
|
31
|
+
_lp_context.set(ctx)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Dict, Optional, Protocol
|
|
6
|
+
|
|
7
|
+
from nonebot.adapters import Bot, Event
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("oblp")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Identity:
|
|
14
|
+
user_id: str
|
|
15
|
+
platform: str
|
|
16
|
+
group_id: Optional[str] = None
|
|
17
|
+
role: str = "member"
|
|
18
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IdentityResolver(Protocol):
|
|
22
|
+
async def resolve(self, bot: Bot, event: Event) -> Identity:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_resolver: Optional[IdentityResolver] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_resolver(resolver: IdentityResolver):
|
|
30
|
+
global _resolver
|
|
31
|
+
_resolver = resolver
|
|
32
|
+
logger.info(f"IdentityResolver set to {type(resolver).__name__}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_resolver() -> Optional[IdentityResolver]:
|
|
36
|
+
return _resolver
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OneBotV11Resolver:
|
|
40
|
+
async def resolve(self, bot: Bot, event: Event) -> Identity:
|
|
41
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent, PrivateMessageEvent
|
|
42
|
+
|
|
43
|
+
user_id = event.get_user_id()
|
|
44
|
+
group_id = None
|
|
45
|
+
role = "member"
|
|
46
|
+
platform = "onebot_v11"
|
|
47
|
+
|
|
48
|
+
if isinstance(event, GroupMessageEvent):
|
|
49
|
+
group_id = str(event.group_id)
|
|
50
|
+
if event.sender and event.sender.role:
|
|
51
|
+
role = event.sender.role
|
|
52
|
+
elif isinstance(event, PrivateMessageEvent):
|
|
53
|
+
platform = "onebot_v11_private"
|
|
54
|
+
|
|
55
|
+
if user_id in getattr(bot.config, "superusers", set()):
|
|
56
|
+
role = "superuser"
|
|
57
|
+
|
|
58
|
+
return Identity(
|
|
59
|
+
user_id=user_id,
|
|
60
|
+
platform=platform,
|
|
61
|
+
group_id=group_id,
|
|
62
|
+
role=role,
|
|
63
|
+
extra={"raw_event_name": event.get_event_name()},
|
|
64
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, Set
|
|
5
|
+
|
|
6
|
+
from nonebot.permission import Permission
|
|
7
|
+
from nonebot.adapters import Bot, Event
|
|
8
|
+
|
|
9
|
+
from ..core.models import ContextSet, QueryOptions, User, PermissionNode
|
|
10
|
+
from ..core.registry import NodeRegistry
|
|
11
|
+
from ..core.engine import PermissionEngine
|
|
12
|
+
from ..core.cache import PermissionCache
|
|
13
|
+
from ..core.context_provider import get_context_providers
|
|
14
|
+
from ..config import oblp_config
|
|
15
|
+
from ..storage import get_store
|
|
16
|
+
from .identity import get_resolver, Identity
|
|
17
|
+
from .context import LPContext, set_context
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("oblp")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _apply_default_nodes(user: User, identity: Identity) -> list:
|
|
23
|
+
"""Apply default permission nodes based on the user's role from config."""
|
|
24
|
+
nodes = []
|
|
25
|
+
role_map = {
|
|
26
|
+
"owner": oblp_config.default_group_owner,
|
|
27
|
+
"admin": oblp_config.default_group_admin,
|
|
28
|
+
"member": oblp_config.default_group_member,
|
|
29
|
+
"superuser": oblp_config.superuser_inherit,
|
|
30
|
+
}
|
|
31
|
+
patterns = role_map.get(identity.role, [])
|
|
32
|
+
for pattern in patterns:
|
|
33
|
+
if pattern == "*":
|
|
34
|
+
nodes.append(PermissionNode(key="*", value=True))
|
|
35
|
+
elif pattern:
|
|
36
|
+
nodes.append(PermissionNode(key=pattern, value=True))
|
|
37
|
+
return nodes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _checker_factory(
|
|
41
|
+
node_keys: Set[str],
|
|
42
|
+
mode: str,
|
|
43
|
+
base_contexts: Optional[ContextSet] = None,
|
|
44
|
+
):
|
|
45
|
+
async def checker(bot: Bot, event: Event) -> bool:
|
|
46
|
+
resolver = get_resolver()
|
|
47
|
+
store = get_store()
|
|
48
|
+
if resolver is None or store is None:
|
|
49
|
+
logger.warning("OBLP not initialized: no resolver or store")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
identity: Identity = await resolver.resolve(bot, event)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Failed to resolve identity: {e}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
ctx = ContextSet.from_dict({
|
|
59
|
+
"platform": identity.platform,
|
|
60
|
+
"user_id": identity.user_id,
|
|
61
|
+
"role": identity.role,
|
|
62
|
+
})
|
|
63
|
+
if identity.group_id:
|
|
64
|
+
ctx = ctx.with_context("group_id", identity.group_id)
|
|
65
|
+
|
|
66
|
+
if base_contexts:
|
|
67
|
+
for k, v in base_contexts.data.items():
|
|
68
|
+
ctx = ctx.with_context(k, v)
|
|
69
|
+
|
|
70
|
+
# Call all registered third-party context providers
|
|
71
|
+
for provider in get_context_providers():
|
|
72
|
+
try:
|
|
73
|
+
extra = await provider(bot, event, ctx)
|
|
74
|
+
if extra:
|
|
75
|
+
for k, v in extra.items():
|
|
76
|
+
ctx = ctx.with_context(k, v)
|
|
77
|
+
except Exception:
|
|
78
|
+
logger.exception(f"ContextProvider {type(provider).__name__} error")
|
|
79
|
+
|
|
80
|
+
query_opts = QueryOptions(
|
|
81
|
+
mode="contextual",
|
|
82
|
+
contexts=ctx,
|
|
83
|
+
flags={"include_inherited", "resolve_inheritance"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
cache_key = PermissionCache.make_key(identity.user_id, ctx)
|
|
87
|
+
cached = PermissionCache.get(cache_key)
|
|
88
|
+
if cached is not None:
|
|
89
|
+
resolved = cached
|
|
90
|
+
else:
|
|
91
|
+
user = await store.get_user(identity.user_id)
|
|
92
|
+
if user is None:
|
|
93
|
+
user = User(user_id=identity.user_id)
|
|
94
|
+
|
|
95
|
+
default_nodes = _apply_default_nodes(user, identity)
|
|
96
|
+
resolved = await user.get_effective_nodes(store, query_opts, ctx)
|
|
97
|
+
|
|
98
|
+
for dn in default_nodes:
|
|
99
|
+
if dn.key not in resolved:
|
|
100
|
+
resolved[dn.key] = dn.value
|
|
101
|
+
|
|
102
|
+
PermissionCache.set(cache_key, resolved)
|
|
103
|
+
|
|
104
|
+
matched_node = None
|
|
105
|
+
if mode == "any":
|
|
106
|
+
result = any(
|
|
107
|
+
PermissionEngine.check(resolved, nk)
|
|
108
|
+
for nk in node_keys
|
|
109
|
+
)
|
|
110
|
+
if result:
|
|
111
|
+
for nk in node_keys:
|
|
112
|
+
if PermissionEngine.check(resolved, nk):
|
|
113
|
+
matched_node = nk
|
|
114
|
+
break
|
|
115
|
+
elif mode == "all":
|
|
116
|
+
result = all(
|
|
117
|
+
PermissionEngine.check(resolved, nk)
|
|
118
|
+
for nk in node_keys
|
|
119
|
+
)
|
|
120
|
+
if result:
|
|
121
|
+
matched_node = ",".join(node_keys)
|
|
122
|
+
else:
|
|
123
|
+
target = next(iter(node_keys))
|
|
124
|
+
result = PermissionEngine.check(resolved, target)
|
|
125
|
+
if result:
|
|
126
|
+
matched_node = target
|
|
127
|
+
|
|
128
|
+
lp_ctx = LPContext(
|
|
129
|
+
user=user if 'user' in dir() else User(user_id=identity.user_id),
|
|
130
|
+
identity=identity,
|
|
131
|
+
query_options=query_opts,
|
|
132
|
+
resolved_nodes=resolved,
|
|
133
|
+
matched_node=matched_node,
|
|
134
|
+
)
|
|
135
|
+
set_context(lp_ctx)
|
|
136
|
+
|
|
137
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
138
|
+
logger.debug(
|
|
139
|
+
f"[oblp] User {identity.user_id} check {node_keys} "
|
|
140
|
+
f"in {ctx.to_dict()} -> {result}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
return checker
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def require(node_key: str, contexts: Optional[ContextSet] = None) -> Permission:
|
|
149
|
+
return Permission(_checker_factory({node_key}, "single", contexts))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def require_any(*node_keys: str, contexts: Optional[ContextSet] = None) -> Permission:
|
|
153
|
+
return Permission(_checker_factory(set(node_keys), "any", contexts))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def require_all(*node_keys: str, contexts: Optional[ContextSet] = None) -> Permission:
|
|
157
|
+
return Permission(_checker_factory(set(node_keys), "all", contexts))
|