Skip to content

Commit

Permalink
new bulk api
Browse files Browse the repository at this point in the history
  • Loading branch information
ValdemarGr committed Feb 7, 2024
1 parent 3116067 commit d00b461
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 17 deletions.
38 changes: 25 additions & 13 deletions client/src/main/scala/spice4s/client/SpiceClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import cats.effect._
import cats.implicits._
import cats._
import com.authzed.api.v1.permission_service.PermissionsServiceFs2Grpc
import com.authzed.api.v1.experimental_service.ExperimentalServiceFs2Grpc
import spice4s.client.models._
import spice4s.client.util._
import fs2.Stream
Expand All @@ -40,6 +41,8 @@ trait SpiceClient[F[_]] { self =>

def lookupSubjects(x: LookupSubjectsRequest): Stream[F, LookupSubjectsResponse]

def bulkCheckPermission(x: BulkCheckPermissionRequest): F[BulkCheckPermissionResponse]

def mapK[G[_]](fk: F ~> G): SpiceClient[G] = new SpiceClient[G] {
override def readRelationships(x: ReadRelationshipsRequest): Stream[G, ReadRelationshipsResponse] =
self.readRelationships(x).translate(fk)
Expand All @@ -61,26 +64,32 @@ trait SpiceClient[F[_]] { self =>

override def lookupSubjects(x: LookupSubjectsRequest): Stream[G, LookupSubjectsResponse] =
self.lookupSubjects(x).translate(fk)

override def bulkCheckPermission(x: BulkCheckPermissionRequest): G[BulkCheckPermissionResponse] =
fk(self.bulkCheckPermission(x))
}
}

object SpiceClient {
val AUTHORIZATION = "authorization"
val METADATA_KEY = Metadata.Key.of(AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER)

def fromChannel[F[_]: Async](channel: Channel, token: String): Resource[F, SpiceClient[F]] =
PermissionsServiceFs2Grpc
.clientResource[F, Unit](
channel,
{ _ =>
val m = new Metadata()
m.put(METADATA_KEY, s"Bearer ${token}")
m
}
)
.map(fromClient[F])

def fromClient[F[_]: MonadThrow](client: PermissionsServiceFs2Grpc[F, Unit]): SpiceClient[F] = {
def fromChannel[F[_]: Async](channel: Channel, token: String): Resource[F, SpiceClient[F]] = {
val metadata = {
val m = new Metadata()
m.put(METADATA_KEY, s"Bearer ${token}")
m
}
(
PermissionsServiceFs2Grpc.clientResource[F, Unit](channel, _ => metadata),
ExperimentalServiceFs2Grpc.clientResource[F, Unit](channel, _ => metadata)
).mapN(fromClient[F])
}

def fromClient[F[_]: MonadThrow](
client: PermissionsServiceFs2Grpc[F, Unit],
experimental: ExperimentalServiceFs2Grpc[F, Unit]
): SpiceClient[F] = {
def decodeWith[A, B](f: A => Decoded[B]): A => F[B] = a => raiseIn[F, B](f(a))

new SpiceClient[F] {
Expand All @@ -104,6 +113,9 @@ object SpiceClient {

def lookupSubjects(x: LookupSubjectsRequest): Stream[F, LookupSubjectsResponse] =
client.lookupSubjects(x.encode, ()).evalMap(decodeWith(LookupSubjectsResponse.decode))

def bulkCheckPermission(x: BulkCheckPermissionRequest): F[BulkCheckPermissionResponse] =
experimental.bulkCheckPermission(x.encode, ()).flatMap(decodeWith(BulkCheckPermissionResponse.decode))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2023 CaseHubDK
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package spice4s.client.models

import spice4s.client.util._
import cats.implicits._
import com.authzed.api.v1.{experimental_service => es}

final case class BulkCheckPermissionPair(
request: BulkCheckPermissionRequestItem,
response: BulkCheckPermissionPair.Response
)

object BulkCheckPermissionPair {
sealed trait Response extends Product with Serializable
object Response {
final case class Item(value: BulkCheckPermissionResponseItem) extends Response
final case class Error(value: com.google.rpc.status.Status) extends Response

def decode(x: es.BulkCheckPermissionPair.Response): Decoded[Response] = {
import es.BulkCheckPermissionPair.{Response => R}
x match {
case R.Item(x) => BulkCheckPermissionResponseItem.decode(x).map(Item.apply)
case R.Error(x) => Error(x).pure[Decoded]
case R.Empty => raise("empty response")
}
}
}

def decode(x: es.BulkCheckPermissionPair): Decoded[BulkCheckPermissionPair] =
(
field("request")(req(x.request) andThen BulkCheckPermissionRequestItem.decode),
field("response")(Response.decode(x.response))
).mapN(BulkCheckPermissionPair.apply)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 CaseHubDK
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package spice4s.client.models

import com.authzed.api.v1.{experimental_service => es}
import cats.data.NonEmptyList

final case class BulkCheckPermissionRequest(
consistency: Option[Consistency] = None,
items: NonEmptyList[BulkCheckPermissionRequestItem]
) {
def encode = es.BulkCheckPermissionRequest.of(
consistency.map(_.encode),
items.map(_.encode).toList
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2023 CaseHubDK
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package spice4s.client.models

import spice4s.client.util._
import cats.implicits._
import com.authzed.api.v1.{experimental_service => es}

final case class BulkCheckPermissionRequestItem(
resource: ObjectReference,
permission: Relation,
subject: SubjectReference
) {
def encode = es.BulkCheckPermissionRequestItem.of(
resource.encode.some,
permission.value,
subject.encode.some,
None
)
}

object BulkCheckPermissionRequestItem {
def decode(x: es.BulkCheckPermissionRequestItem): Decoded[BulkCheckPermissionRequestItem] =
(
field("resource")(req(x.resource) andThen ObjectReference.decode),
field("permission")(Relation.decode(x.permission) andThen req),
field("subject")(req(x.subject) andThen SubjectReference.decode)
).mapN(BulkCheckPermissionRequestItem.apply)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2023 CaseHubDK
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package spice4s.client.models

import spice4s.client.util._
import cats.implicits._
import com.authzed.api.v1.{experimental_service => es}

final case class BulkCheckPermissionResponse(
checkedAt: Option[ZedToken],
pairs: List[BulkCheckPermissionPair]
)

object BulkCheckPermissionResponse {
def decode(x: es.BulkCheckPermissionResponse): Decoded[BulkCheckPermissionResponse] =
(
field("checkedAt")(x.checkedAt.flatTraverse(ZedToken.decode)),
field("pairs")(indexed(x.pairs.toList.map(BulkCheckPermissionPair.decode)))
).mapN(BulkCheckPermissionResponse.apply)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2023 CaseHubDK
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package spice4s.client.models

import spice4s.client.util._
import com.authzed.api.v1.{experimental_service => es}

final case class BulkCheckPermissionResponseItem(
permissionship: CheckPermissionResponse.Permissionship
)

object BulkCheckPermissionResponseItem {
def decode(x: es.BulkCheckPermissionResponseItem): Decoded[BulkCheckPermissionResponseItem] =
(
field("permissionship")(CheckPermissionResponse.Permissionship.decode(x.permissionship))
).map(BulkCheckPermissionResponseItem.apply)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final case class RelationshipFilter(
def encode = ps.RelationshipFilter.of(
resourceType.value,
resourceId.foldMap(_.value),
"",
relation.foldMap(_.value),
subjectFilter.map(_.encode)
)
Expand Down
14 changes: 14 additions & 0 deletions proto/src/main/protobuf/authzed/api/v1/error_reason.proto
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,18 @@ enum ErrorReason {
// }
// }
ERROR_REASON_MAXIMUM_DEPTH_EXCEEDED = 19;

// The request failed due to a serialization error in the backend database.
// This typically indicates that various in flight transactions conflicted with each other
// and the database had to abort one or more of them. SpiceDB will retry a few times before returning
// the error to the client.
//
// Example of an ErrorInfo:
//
// {
// "reason": "ERROR_REASON_SERIALIZATION_FAILURE",
// "domain": "authzed.com",
// "metadata": {}
// }
ERROR_REASON_SERIALIZATION_FAILURE = 20;
}
54 changes: 54 additions & 0 deletions proto/src/main/protobuf/authzed/api/v1/experimental_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ option java_package = "com.authzed.api.v1";

import "google/api/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/struct.proto";
import "google/rpc/status.proto";

import "authzed/api/v1/core.proto";
import "authzed/api/v1/permission_service.proto";
Expand Down Expand Up @@ -41,6 +43,54 @@ service ExperimentalService {
body: "*"
};
}

rpc BulkCheckPermission(BulkCheckPermissionRequest)
returns (BulkCheckPermissionResponse) {
option (google.api.http) = {
post: "/v1/experimental/permissions/bulkcheckpermission"
body: "*"
};
}
}

message BulkCheckPermissionRequest {
Consistency consistency = 1;

repeated BulkCheckPermissionRequestItem items = 2 [ (validate.rules).repeated .items.message.required = true ];
}

message BulkCheckPermissionRequestItem {
ObjectReference resource = 1 [ (validate.rules).message.required = true ];

string permission = 2 [ (validate.rules).string = {
pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$",
max_bytes : 64,
} ];

SubjectReference subject = 3 [ (validate.rules).message.required = true ];

google.protobuf.Struct context = 4 [ (validate.rules).message.required = false ];
}

message BulkCheckPermissionResponse {
ZedToken checked_at = 1 [ (validate.rules).message.required = false ];

repeated BulkCheckPermissionPair pairs = 2 [ (validate.rules).repeated .items.message.required = true ];
}

message BulkCheckPermissionPair {
BulkCheckPermissionRequestItem request = 1;
oneof response {
BulkCheckPermissionResponseItem item = 2;
google.rpc.Status error = 3;
}
}

message BulkCheckPermissionResponseItem {

CheckPermissionResponse.Permissionship permissionship = 1 [ (validate.rules).enum = {defined_only: true, not_in: [0]} ];

PartialCaveatInfo partial_caveat_info = 2 [ (validate.rules).message.required = false ];
}

// BulkImportRelationshipsRequest represents one batch of the streaming
Expand Down Expand Up @@ -73,6 +123,10 @@ message BulkExportRelationshipsRequest {
// should resume being returned. The cursor can be found on the
// BulkExportRelationshipsResponse object.
Cursor optional_cursor = 3;

// optional_relationship_filter, if specified, indicates the
// filter to apply to each relationship to be exported.
RelationshipFilter optional_relationship_filter = 4;
}

// BulkExportRelationshipsResponse is one page in a stream of relationship
Expand Down
Loading

0 comments on commit d00b461

Please sign in to comment.