Skip to content
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

V3 casl 561 query tags #370

Merged
merged 8 commits into from
Oct 28, 2024
Merged

V3 casl 561 query tags #370

merged 8 commits into from
Oct 28, 2024

Conversation

Amaneusz
Copy link
Collaborator

No description provided.

Signed-off-by: Jakub Amanowicz <[email protected]>
Signed-off-by: Jakub Amanowicz <[email protected]>
Signed-off-by: Jakub Amanowicz <[email protected]>
@@ -160,6 +161,80 @@ class WhereClauseBuilder(private val request: ReadFeatures) {
}
}

private fun whereTags() {
request.query.tags?.let { tagQuery ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why .let ? Maybe I'm old-fashioned, but it looks a little weird, as I understand it replaces if(tags != null), but I had to read it twice to understand :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are totally right, this is not clear - will change

Signed-off-by: Jakub Amanowicz <[email protected]>
*
* By default, (if no special prefix is found) tag is normalized with NFD, lowercased, cleaned of non-ASCII and splittable.
*/
object TagNormalizer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use object, I removed it everywhere. If this should be a singleton, use the following syntax:

@JsExport
class TagNormalizer private constructor() {
  companion object TagNormalizer_C {
    @JvmStatic
    @JsStatic
    fun foo() {}
  }
}

The reason are plenty of bugs in the JavaScript compiler/linker combination. Sadly, the compiler (for objects) automatically names the companion object, and this naming does not work very well, when used across different modules, because somehow the linker messes up naming. We need to open a ticket with Jetbrains about it, but I would first want to have a working code, then to show the issues in the working code, so they can simply checkout, compile and debug.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Signed-off-by: Jakub Amanowicz <[email protected]>
val i = normalizedTag.indexOf('=')
val key: String
val value: Any?
if (i > 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail for a=1, because in this case index will be 1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, fixed & tested

private fun resolveSingleTagQuery(tagQuery: TagQuery) {
when (tagQuery) {
is TagExists -> {
where.append("$tagsAsJsonb ?? '${tagQuery.name}'")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is prone to SQL injections and ASCII zero injections. Values that are provided by the client, like tag-names, must only be used in prepared-statements! You can quote this alternatively, still, it may have a bad impact on performance, when it is executed multiple times, because it can't be cached by the query parser (very likely does not matter in this case, because tag queries will be slow anyway).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very well spotted again - fixed & tested.
Currently, all values from queries (as well as already persisted tags) are used via prepared statement

}

is TagValueIsNull -> {
where.append("${tagValue(tagQuery)} = null")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does not work, it need to be something IS NULL, NULL is like NaN, any value compared against it will be false (well, not exactly, but it is good to remember it like this).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 fixed


is TagValueMatches -> {
val regex = tagQuery.regex
where.append("$tagsAsJsonb @?? '\$.${tagQuery.name} ? (@ like_regex \"${regex}\")'")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is prone to SQL and ASCII zero injections, because not correctly escaped (remember, tags that start with @ allow all unicode characters! For regular expression, we should review carefully, if there is no way of code execution or query manipulation.

return when (castTo) {
null -> "$tagsAsJsonb->'${tagQuery.name}'"
PgType.STRING -> "$tagsAsJsonb->>'${tagQuery.name}'"
else -> "($tagsAsJsonb->'${tagQuery.name}')::${castTo.value}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this prone to SQL injections and ASCII zero attacks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped this in prepared statement as so:

private fun selectTagValue(tagQuery: TagQuery, castTo: PgType? = null): String {
    val tagKeyPlaceholder = placeholderForArg(tagQuery.name, PgType.STRING)
    return when (castTo) {
        null -> "$tagsAsJsonb->$tagKeyPlaceholder"
        PgType.STRING -> "$tagsAsJsonb->>$tagKeyPlaceholder"
        else -> "($tagsAsJsonb->$tagKeyPlaceholder)::${castTo.value}"
    }
}

The tag key will be wrapped as $N and replaced in prepared statement.
The tagsAsJsonb alone is safe:

companion object {
    private val tagsAsJsonb = "naksha_tags(${PgColumn.tags}, ${PgColumn.flags})"
}```

*
* By default, (if no special prefix is found) tag is normalized with NFD, lowercased, cleaned of non-ASCII and splittable.
*/
class TagNormalizer private constructor() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this class is part of lib-models, and it is a public class, it must be exposed to javascript via @JsExport annotation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/**
* Main method for normalized tag splitting. See[TagNormalizer] doc for more
*/
fun splitNormalizedTag(normalizedTag: String): Pair<String, Any?> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pair is not exportable to JS, do we need to keep the method public? If not, just make it private or internal, this should solve the issue I would say. Otherwise, you can use kotlin.collections.Map.Entry or its implementation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal should do the trick - changed

@xeus2001 xeus2001 merged commit e40da57 into v3 Oct 28, 2024
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants