syntax = "proto3";

package nominal.authorization.markings.v1;

import "buf/validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "nominal/protos/gen/v1/alias.proto";
import "nominal/protos/scout/elements/v1/elements.proto";

message AuthorizedGroups {
  repeated string group_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.GroupRid"}];
}

message Marking {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
  // Human-readable identifier for this marking.
  // Must be unique within the organization.
  // Must be lower case alphanumeric characters, optionally separated by hyphens.
  string id = 2 [(buf.validate.field).string.pattern = "^[a-z][a-z0-9-]*$"];
  string description = 3;
  // Group RIDs that are authorized to access data sources with this marking.
  AuthorizedGroups authorized_groups = 4;

  // Symbol and color to identify markings
  optional nominal.scout.elements.v1.Symbol symbol = 5;
  optional nominal.scout.elements.v1.Color color = 6;

  google.protobuf.Timestamp created_at = 7;
  google.protobuf.Timestamp updated_at = 8;

  bool is_archived = 9;
}

// A leaner representation of a Marking that doesn't include the authorized groups.
message MarkingMetadata {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
  // Human-readable identifier for this marking.
  // Must be unique within the organization.
  // Must be lower case alphanumeric characters, optionally separated by hyphens.
  string id = 2 [(buf.validate.field).string.pattern = "^[a-z][a-z0-9-]*$"];
  string description = 3;

  // Symbol and color to identify markings
  optional nominal.scout.elements.v1.Symbol symbol = 4;
  optional nominal.scout.elements.v1.Color color = 5;

  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;

  bool is_archived = 8;
}

message CreateMarkingRequest {
  // Human-readable identifier for this marking.
  // Must be unique within the organization.
  // Must be lower case alphanumeric characters, optionally separated by hyphens.
  string id = 1 [(buf.validate.field).string.pattern = "^[a-z][a-z0-9-]*$"];
  string description = 2;
  // Group RIDs that are authorized to access data sources with this marking.
  AuthorizedGroups authorized_groups = 3;

  optional nominal.scout.elements.v1.Symbol symbol = 4;
  optional nominal.scout.elements.v1.Color color = 5;
}
message CreateMarkingResponse {
  Marking marking = 1;
}

message GetMarkingRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message GetMarkingResponse {
  Marking marking = 1;
}

message GetMarkingByIdRequest {
  string id = 1 [(buf.validate.field).string.pattern = "^[a-z][a-z0-9-]*$"];
}
message GetMarkingByIdResponse {
  Marking marking = 1;
}

message BatchGetMarkingsRequest {
  repeated string marking_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message BatchGetMarkingsResponse {
  repeated Marking markings = 1;
}

message BatchGetMarkingMetadataRequest {
  repeated string marking_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message BatchGetMarkingMetadataResponse {
  repeated MarkingMetadata marking_metadatas = 1;
}

message GetAuthorizedGroupsByMarkingRequest {
  repeated string marking_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message GetAuthorizedGroupsByMarkingResponse {
  map<string, AuthorizedGroups> authorized_groups_by_marking = 1 [(nominal.gen.v1.alias) = {key_java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}

message SearchMarkingsQuery {
  oneof query {
    option (buf.validate.oneof).required = true;
    // Search for markings with an ID that contains the given query as a substring.
    string id_exact_substring_search = 1;
    SearchMarkingsQueryList and = 2;
    SearchMarkingsQueryList or = 3;
  }
}
message SearchMarkingsQueryList {
  repeated SearchMarkingsQuery queries = 1;
}
message SearchMarkingsRequest {
  SearchMarkingsQuery query = 1;
  // Page sizes greater than 1_000 will be rejected. Default pageSize is 100.
  optional int32 page_size = 2 [
    (buf.validate.field).int32.gte = 1,
    (buf.validate.field).int32.lte = 1000
  ];
  optional string next_page_token = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
}
message SearchMarkingsResponse {
  repeated MarkingMetadata marking_metadatas = 1;
  optional string next_page_token = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
}

message UpdateMarkingRequest {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
  // Human-readable identifier for this marking.
  // Must be unique within the organization.
  // Must be lower case alphanumeric characters, optionally separated by hyphens.
  optional string id = 2 [(buf.validate.field).string.pattern = "^[a-z][a-z0-9-]*$"];
  optional string description = 3;
  // Group RIDs that are authorized to access data sources with this marking.
  optional AuthorizedGroups authorized_groups = 4;

  optional UpdateMarkingSymbolWrapper symbol = 5;
  optional UpdateMarkingColorWrapper color = 6;

  message UpdateMarkingSymbolWrapper {
    optional nominal.scout.elements.v1.Symbol value = 1;
  }
  message UpdateMarkingColorWrapper {
    optional nominal.scout.elements.v1.Color value = 1;
  }
}

message UpdateMarkingResponse {
  Marking marking = 1;
}

message ArchiveMarkingsRequest {
  repeated string marking_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message ArchiveMarkingsResponse {
  // intentionally empty
}

message UnarchiveMarkingsRequest {
  repeated string marking_rids = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message UnarchiveMarkingsResponse {
  // intentionally empty
}

message UpdateMarkingsOnResourceRequest {
  string resource = 1 [(nominal.gen.v1.alias) = {java_name: "com.palantir.ri.ResourceIdentifier"}];
  repeated string markings_to_apply = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
  repeated string markings_to_remove = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
}
message UpdateMarkingsOnResourceResponse {
  // intentionally empty
}

message GetResourcesForMarkingRequest {
  string marking_rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
  // Page sizes greater than 1_000 will be rejected. Default pageSize is 100.
  optional int32 page_size = 2 [
    (buf.validate.field).int32.gte = 1,
    (buf.validate.field).int32.lte = 1000
  ];
  optional string next_page_token = 3 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
}
message GetResourcesForMarkingResponse {
  repeated MarkedResource resources = 1;
  optional string next_page_token = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];

  message MarkedResource {
    // nb: skipping field 1 in order to keep the door open to unify this message with "AppliedMarking" below
    string resource = 2 [(nominal.gen.v1.alias) = {java_name: "com.palantir.ri.ResourceIdentifier"}];
    google.protobuf.Timestamp applied_at = 3;
  }
}

message GetMarkingsForResourcesRequest {
  // The resources to get the markings for.
  repeated string resources = 1 [(nominal.gen.v1.alias) = {java_name: "com.palantir.ri.ResourceIdentifier"}];
}
message GetMarkingsForResourcesResponse {
  // A map from resource ID to the markings applied to them.
  map<string, ResourceMarkingsList> resource_to_markings = 1 [(nominal.gen.v1.alias) = {key_java_name: "com.palantir.ri.ResourceIdentifier"}];

  message ResourceMarkingsList {
    repeated AppliedMarking applied_markings = 1;

    message AppliedMarking {
      string marking_rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.MarkingRid"}];
      // nb: skipping field 2 in order to keep the door open to unify this message with "MarkedResource" above
      google.protobuf.Timestamp applied_at = 3;
    }

    // TODO(MGMT-491): for Default Markings, we may add a field here for any default markings that
    //  are not explicitly applied to the resource.
  }
}

// TODO(MGMT-481): define and use grpc errors.

// This service manages Markings, which are a mechanism to restrict access to data sources.
service MarkingService {
  // Creates a marking.
  // Throws HTTP 409 / gRPC 6 if a marking with the same ID already exists.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to create a marking.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to read some of the specified groups.
  rpc CreateMarking(CreateMarkingRequest) returns (CreateMarkingResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1"
      body: "*"
      response_body: "marking"
    };
  }
  // Gets a marking by its RID.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to read the marking.
  // Throws HTTP 403 / gRPC 5 if the marking is not found.
  rpc GetMarking(GetMarkingRequest) returns (GetMarkingResponse) {
    option (google.api.http) = {
      get: "/authorization/markings/v1/{rid}"
      response_body: "marking"
    };
  }
  // Gets a marking by its ID, a human-readable identifier unique within the org.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to read the marking.
  // Throws HTTP 403 / gRPC 5 if the marking is not found.
  rpc GetMarkingById(GetMarkingByIdRequest) returns (GetMarkingByIdResponse) {
    option (google.api.http) = {
      get: "/authorization/markings/v1/id/{id}"
      response_body: "marking"
    };
  }
  // Gets multiple markings by RID.
  // If a marking is not found or the user doesn't have permission to read it,
  // it will not be included in the response.
  rpc BatchGetMarkings(BatchGetMarkingsRequest) returns (BatchGetMarkingsResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/batch-get"
      body: "*"
      response_body: "*"
    };
  }
  // Gets metadata for multiple markings by RID. Metadata does not include the authorized groups.
  // If a marking is not found or the user doesn't have permission to read it,
  // it will not be included in the response.
  rpc BatchGetMarkingMetadata(BatchGetMarkingMetadataRequest) returns (BatchGetMarkingMetadataResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/batch-get-metadata"
      body: "*"
      response_body: "*"
    };
  }
  // Gets the authorized Groups for each of a set of Markings.
  // Markings that are not found or for which the user does not have permission to read
  // will be filtered from the response.
  // Groups for which the user does not have permission to read will be filtered from the response.
  rpc GetAuthorizedGroupsByMarking(GetAuthorizedGroupsByMarkingRequest) returns (GetAuthorizedGroupsByMarkingResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/authorized-groups-by-marking"
      body: "*"
      response_body: "*"
    };
  }
  // Search Markings, returning paged results.
  // Throws HTTP 400 / gRPC 3 if the query is incorrectly formed.
  rpc SearchMarkings(SearchMarkingsRequest) returns (SearchMarkingsResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/search"
      body: "*"
      response_body: "*"
    };
  }
  // Updates a marking.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to update the marking.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to read any of the specified groups.
  // Throws HTTP 403 / gRPC 5 if the marking is not found.
  // Throws HTTP 409 / gRPC 6 if trying to update the ID of a marking to an ID that already exists.
  // Throws HTTP 400 / gRPC 3 if trying to update an archived marking.
  rpc UpdateMarking(UpdateMarkingRequest) returns (UpdateMarkingResponse) {
    option (google.api.http) = {
      put: "/authorization/markings/v1/{rid}"
      body: "*"
      response_body: "marking"
    };
  }
  // Archives markings. Archiving a marking is an idempotent operation that prevents further changes
  // or applications of the marking.
  // Archived markings will not appear in search but can be retrieved by `get` endpoints.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to archive one or more of the markings
  // or if any of the markings are not found.
  // Throws HTTP 400 / gRPC 3 if the archived marking is currently applied to one or more resources.
  // TODO(MGMT-489): this will throw 403 if any of the Markings don't exist, but we should throw 404 instead.
  rpc ArchiveMarkings(ArchiveMarkingsRequest) returns (ArchiveMarkingsResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/archive-batch"
      body: "marking_rids"
    };
  }
  // Unarchives markings. Restores ability to apply and modify marking.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to unarchive one or more of the markings
  // or if any of the markings are not found.
  // TODO(MGMT-489): this will throw 403 if any of the Markings don't exist, but we should throw 404 instead.
  rpc UnarchiveMarkings(UnarchiveMarkingsRequest) returns (UnarchiveMarkingsResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/unarchive-batch"
      body: "marking_rids"
    };
  }
  // Update the markings that are applied on a resource. Supports applying and removing markings at once.
  // This endpoint can be used to atomically replace the markings on a resource, or bulk apply/remove markings.
  // This operation is idempotent and will not throw an error if the marking is already applied/removed on the resource.
  // If a resource marking pair is present in both `markings_to_apply` and `markings_to_remove`, this will throw.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to change the markings on the resource.
  // Throws HTTP 400 / gRPC 3 if trying to apply an archived marking.
  rpc UpdateMarkingsOnResource(UpdateMarkingsOnResourceRequest) returns (UpdateMarkingsOnResourceResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/resource/{resource}/markings"
      body: "*"
    };
  }
  // Gets the resources that a Marking is applied to. This endpoint is paginated.
  // Any unauthorized resources will be filtered from the response.
  // Throws HTTP 403 / gRPC 7 if the user does not have permission to read the marking.
  rpc GetResourcesForMarking(GetResourcesForMarkingRequest) returns (GetResourcesForMarkingResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/{marking_rid}/resources"
      body: "*"
      response_body: "*"
    };
  }
  // Gets the requested resources and the markings applied to them. Since the number of markings applied
  // to a resource is typically small, this endpoint will return all markings for each resource.
  // Any unauthorized resources or markings will be filtered from the response.
  rpc GetMarkingsForResources(GetMarkingsForResourcesRequest) returns (GetMarkingsForResourcesResponse) {
    option (google.api.http) = {
      post: "/authorization/markings/v1/markings-for-resources"
      body: "*"
      response_body: "*"
    };
  }
}
