-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: optimize API key storage with bitflags implementation #5495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: key-actions-to-bitflags-mask
Are you sure you want to change the base?
feat: optimize API key storage with bitflags implementation #5495
Conversation
Hey @CodeMan62, Thank you for jumping on my issue, I wasn't expecting it to be as fast! 🏎️ The first part of the job has not been merged yet: It may not be convenient for you to work on top of another branch so let's wait for the upper linked PR to be merged before merging yours, it should be merged this week. 😄 I quickly checked your implementation, and I think it meets the requirements, but I'll need to investigate a bit more. Thank you again for your commitment! see you! |
any updates on this @ManyTheFish |
Hey @CodeMan62, The previous link is a reference branch containing the first part of the work, adding the bitflags to the code using the If you are still interested in contributing to this feature, first, you should rebase your work on the
We will merge this into the After that, we will have to support the version upgrade of Meilisearch, which will be the third part. 😄 Let me know if something isn't clear enough or if you have any additional questions! See you! |
@ManyTheFish hey, it will take a bit of time, but it will be done soon just give me some time |
b572284
to
618df7c
Compare
Hey @ManyTheFish can you tell me that the rebase I have done is right or not, and also, do I still need to do this??
|
@ManyTheFish I am commiting with a error it would be helpful if you can help me in that and also there will be almost all required CI's will fail due to that |
@ManyTheFish can you answer my question in the requested changes so that we can do further work in this PR |
@ManyTheFish can you tell me if anything remains to implement or we can move on to fix tests?? |
Hey @CodeMan62,
For the second step, you'll have to refactor the pub fn get_expiration_date(
&self,
uid: Uuid,
- action: Action,
+ bitflags: u32,
index: Option<&str>,
) -> Result<Option<Option<OffsetDateTime>>> { Compare the bitflags directly. The goal here is to be able to require multiple actions for the same route, so the
|
got it |
Converting this to a draft once the work will be done I will let you know @ManyTheFish |
@ManyTheFish I think we have finished part 2 Is it done or anything left in it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @CodeMan62,
Here is my feedback:
prefix_first_expiration_date
should not exist anymore- the
Option<Index>
given tois_key_authorized
is a restriction,None
means there is no index restriction. has_action
function should be replaced by a bitflag comparisonget_expiration_date
should be replaced by a newis_key_authorized
function
This new is_key_authorized
should:
- check if the key_flags is a superset of the required flag, if not, return refused
- check if the key authorized indexes contain the required index, if not, return refused, skip if required index is None
- check if the expiration_date is not expired, if expired, return refused
- if all previous checks are ok, return
OK
crates/meilisearch-auth/src/store.rs
Outdated
fn has_action(&self, bitflags: u32) -> bool { | ||
for (index, action_flag) in enum_iterator::all::<Action>().enumerate() { | ||
if action_flag.bits() == bitflags && (self.bitflags & (1 << index) != 0) { | ||
return true; | ||
} | ||
} | ||
false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To remove in favor of a bitflag comparison:
fn has_action(&self, bitflags: u32) -> bool { | |
for (index, action_flag) in enum_iterator::all::<Action>().enumerate() { | |
if action_flag.bits() == bitflags && (self.bitflags & (1 << index) != 0) { | |
return true; | |
} | |
} | |
false | |
} | |
fn is_bitflag_authorized(&self, bitflags: u32) -> bool { | |
bitflags.difference(self.bitflags).is_empty() | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is no method difference
in u32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, but wouldn't bitflags & self.bitflags == 0
works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
huhh i missed it thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small warning here, an intersection is not what I want, this will not ensure that the key bitflags are a superset of the required bitflags.
Only a difference should work.
The solution may reside in one of the bitflags crate types or traits.
The Flags trait has a difference
method, if we implement the function as follows:
fn is_bitflag_authorized<F: Flags>(&self, bitflags: F) -> bool {
bitflags.difference(self.bitflags).is_empty()
}
It would work if self.bitflags
implements the Flags
trait as well.
I must agree on something, the crate documentation misses some hints and examples. 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think then we need to import bitflags
crate here if we want to use Flags trait or should I implement something like this??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's already imported, it's used for the Actions
, when doing action_flag.bits()
, you are using the Flags
trait.
Maybe, we should use the type Action everywhere, because it seems that it can contain "unnamed" flags that could correspond to the union of several Actions.
fn is_bitflag_authorized(&self, bitflags: Action) -> bool {
// self.bitflag has type: Action
bitflags.difference(self.bitflags).is_empty()
}
this may work 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey
fn is_bitflag_authorized(&self, bitflags: u32) -> bool {
Action::from_bits(bitflags)
.map(|flags| flags.difference(Action::from_bits(self.bitflags).unwrap_or(Action::empty())).is_empty())
.unwrap_or(false)
}
I have tried this and I think this is good to go and we don't need to change type of bitflags to Action
I have pushed it tell me if it seems wrong
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crates/meilisearch-auth/src/store.rs
Outdated
pub fn is_key_authorized( | ||
&self, | ||
uid: Uuid, | ||
action: Action, | ||
bitflags: u32, | ||
index: Option<&str>, | ||
) -> Result<Option<Option<OffsetDateTime>>> { | ||
) -> Result<AuthorizationStatus> { | ||
let rtxn = self.env.read_txn()?; | ||
let tuple = (&uid, &action, index.map(|s| s.as_bytes())); | ||
match self.action_keyid_index_expiration.get(&rtxn, &tuple)? { | ||
Some(expiration) => Ok(Some(expiration)), | ||
None => { | ||
let tuple = (&uid, &action, None); | ||
for result in self.action_keyid_index_expiration.prefix_iter(&rtxn, &tuple)? { | ||
let ((_, _, index_uid_pattern), expiration) = result?; | ||
if let Some((pattern, index)) = index_uid_pattern.zip(index) { | ||
let index_uid_pattern = str::from_utf8(pattern)?; | ||
let pattern = IndexUidPattern::from_str(index_uid_pattern) | ||
.map_err(|e| AuthControllerError::Internal(Box::new(e)))?; | ||
if pattern.matches_str(index) { | ||
return Ok(Some(expiration)); | ||
} | ||
} | ||
} | ||
Ok(None) | ||
|
||
// Get key info from database | ||
let key_masks = match self.key_actions.get(&rtxn, uid.as_bytes())? { | ||
Some(key_masks) => key_masks, | ||
None => return Ok(AuthorizationStatus::Refused), | ||
}; | ||
|
||
// Check if the key's bitflags contain the required bitflags | ||
if !key_masks.is_bitflag_authorized(bitflags) { | ||
return Ok(AuthorizationStatus::Refused); | ||
} | ||
|
||
// Check if the key is authorized for the requested index | ||
if let Some(index_str) = index { | ||
let index_uid = match IndexUid::try_from(String::from(index_str)) { | ||
Ok(uid) => uid, | ||
Err(_) => return Ok(AuthorizationStatus::Refused), | ||
}; | ||
|
||
let authorized_for_index = | ||
key_masks.indexes.iter().any(|pattern| pattern.matches(&index_uid)); | ||
if !authorized_for_index { | ||
return Ok(AuthorizationStatus::Refused); | ||
} | ||
} | ||
} | ||
|
||
pub fn prefix_first_expiration_date( | ||
&self, | ||
uid: Uuid, | ||
action: Action, | ||
) -> Result<Option<Option<OffsetDateTime>>> { | ||
let rtxn = self.env.read_txn()?; | ||
let tuple = (&uid, &action, None); | ||
let exp = self | ||
.action_keyid_index_expiration | ||
.prefix_iter(&rtxn, &tuple)? | ||
.next() | ||
.transpose()? | ||
.map(|(_, expiration)| expiration); | ||
|
||
Ok(exp) | ||
// Check if the key is expired | ||
if let Some(expires_at) = key_masks.expires_at { | ||
if expires_at < OffsetDateTime::now_utc() { | ||
return Ok(AuthorizationStatus::Refused); | ||
} | ||
} | ||
|
||
// All checks passed | ||
Ok(AuthorizationStatus::Ok) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect!
Clean code, thank you for the clarity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haha after long time someone gave compliment to my code 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @CodeMan62,
Last review before going to the next part:
- Small documentation change
- can you make the test work and see if there is nothing wrong.
Example of failing test
Snapshot: delete_api_key
Source: crates/meilisearch/tests/auth/api_keys.rs:922
────────────────────────────────────────────────────────────────────────────────
-old snapshot
+new results
────────────┬───────────────────────────────────────────────────────────────────
0 0 │ {
1 │- "name": null,
2 │- "description": "Indexing API key",
3 │- "key": "[ignored]",
4 │- "uid": "[ignored]",
5 │- "actions": [
6 │- "search",
7 │- "documents.add",
8 │- "documents.get",
9 │- "documents.delete",
10 │- "indexes.create",
11 │- "indexes.get",
12 │- "indexes.update",
13 │- "indexes.delete",
14 │- "tasks.get",
15 │- "settings.get",
16 │- "settings.update",
17 │- "stats.get",
18 │- "dumps.create"
19 │- ],
20 │- "indexes": [
21 │- "products"
22 │- ],
23 │- "expiresAt": "2050-11-13T00:00:00Z",
24 │- "createdAt": "[ignored]",
25 │- "updatedAt": "[ignored]"
1 │+ "message": "task 45 panicked with message \"an action is missing a matching serialized value\"",
2 │+ "code": "internal",
3 │+ "type": "internal",
4 │+ "link": "https://docs.meilisearch.com/errors#internal"
26 5 │ }
────────────┴───────────────────────────────────────────────────────────────────
Stopped on the first failure. Run `cargo insta test` to run all snapshots.
Next
If the upper point are fixed, we will merge this branch and continue the work on another PR.
Global steps:
- Adapt your changes to the previous work
- Adapt the authentication to compare masks and not actions
- Modify this guarded databy a mask requesting DocumentsAdd and DocumentsDelete
- Remove all the unnecessary
All
action, bitmasks should do the job.
|
||
impl KeyMasks { | ||
fn is_bitflag_authorized(&self, bitflags: Action) -> bool { | ||
bitflags.difference(self.bitflags).is_empty() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bitflags.difference(self.bitflags).is_empty() | |
// Authorization succeeds when the key's permissions contain all requested permissions. | |
// If any requested permission is missing, the difference will be non-empty. | |
bitflags.difference(self.bitflags).is_empty() |
Pull Request
Related issue
Fixes #5489
What does this PR do?
KeyActions
struct to efficiently store permissions as bitflagsTesting Done:
Manual Testing:
Automated Testing:
cargo test -p meilisearch-auth
PR checklist
Please check if your PR fulfills the following requirements: