syntax = "proto3";

package nominal.authentication.groups.v1;

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

// A group represents a collection of users within an organization
message Group {
  string rid = 1 [(nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.GroupRid"}];

  // Required on create requests; immutable on updates.
  string org_rid = 2 [
    (nominal.gen.v1.alias) = {java_name: "io.nominal.authentication.api.OrgRid"},
    (google.api.field_behavior) = IMMUTABLE
  ];

  // Group ID, unique per org. If sourced from the customer's IdP, will match IdP group name.
  // Required on create requests; immutable on updates.
  string group_id = 3 [(google.api.field_behavior) = IMMUTABLE];

  string display_name = 4;
  string description = 5;

  // The list of users synced to this group
  repeated string user_rids = 6 [
    (google.api.field_behavior) = OUTPUT_ONLY,
    (nominal.gen.v1.alias) = {java_name: "io.nominal.authentication.api.UserRid"}
  ];

  google.protobuf.Timestamp created_at = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
  google.protobuf.Timestamp updated_at = 8 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Symbol to identify groups
  optional nominal.scout.elements.v1.Symbol symbol = 9;
}

message SearchGroupsQuery {
  oneof query {
    string exact_substring_text = 1;
    // and is implemented to allow for list (use SearchQueryAnd([]))
    SearchQueryAnd and = 2;
    SearchQueryOr or = 3;
  }
}

message SearchQueryAnd {
  repeated SearchGroupsQuery queries = 1;
}

message SearchQueryOr {
  repeated SearchGroupsQuery queries = 1;
}

message SearchGroupsRequest {
  // Page sizes greater than 1_000 will be rejected. Default pageSize is 100.
  optional int32 page_size = 1;
  optional string next_page_token = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
  SearchGroupsQuery query = 3 [(buf.validate.field) = {required: true}];
  // omitting archivedStatus because currently groups are IDP sync-only
}

message SearchGroupsResponse {
  repeated Group results = 1;
  optional string next_page_token = 2 [(nominal.gen.v1.alias) = {java_name: "io.nominal.api.Token"}];
}

message GetGroupRequest {
  string group_rid = 1 [
    (nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.GroupRid"},
    (buf.validate.field) = {required: true}
  ];
}

message GetGroupResponse {
  Group group = 1 [(buf.validate.field) = {required: true}];
}

message GetGroupByIdRequest {
  string group_id = 1 [(buf.validate.field) = {required: true}];
}

message GetGroupByIdResponse {
  Group group = 1 [(buf.validate.field) = {required: true}];
}

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

message GetGroupsResponse {
  map<string, Group> groups = 1 [(nominal.gen.v1.alias) = {key_java_name: "io.nominal.scout.rids.api.GroupRid"}];
}

message UpdateGroupMetadataRequest {
  optional string display_name = 1;
  optional string description = 2;
  optional UpdateGroupSymbolWrapper symbol = 3;

  message UpdateGroupSymbolWrapper {
    optional nominal.scout.elements.v1.Symbol value = 1;
  }
}

message UpdateGroupMetadataRequestWrapper {
  string group_rid = 1 [
    (nominal.gen.v1.alias) = {java_name: "io.nominal.scout.rids.api.GroupRid"},
    (buf.validate.field) = {required: true}
  ];

  UpdateGroupMetadataRequest request = 2 [(buf.validate.field) = {required: true}];
}

message UpdateGroupMetadataResponse {
  Group group = 1 [(buf.validate.field) = {required: true}];
}

message CreateOrUpdateGroupRequest {
  Group group = 1 [(buf.validate.field) = {required: true}];
}

message CreateOrUpdateGroupResponse {
  Group group = 1 [(buf.validate.field) = {required: true}];
}

enum GroupErrors {
  // buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX
  GROUP_ERRORS_UNAUTHORIZED = 0 [(nominal.gen.v1.error) = {
    code: "PERMISSION_DENIED"
    message: "Not authorized for group"
  }];
  GROUP_ERRORS_NOT_FOUND = 1 [(nominal.gen.v1.error) = {
    code: "NOT_FOUND"
    message: "Group not found"
  }];
  GROUP_ERRORS_REQUESTED_PAGE_SIZE_TOO_LARGE = 2 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Requested page size is too large"
  }];
  GROUP_ERRORS_ID_CONFLICT = 3 [(nominal.gen.v1.error) = {
    code: "ALREADY_EXISTS"
    message: "Group with given group_id already exists in organization"
  }];
  GROUP_ERRORS_IDENTITY_MISMATCH = 4 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Request identity field does not match the existing group"
  }];
  GROUP_ERRORS_MISSING_REQUIRED_FIELD = 5 [(nominal.gen.v1.error) = {
    code: "INVALID_ARGUMENT"
    message: "Required field missing on request"
  }];
}

// Service for managing groups within an organization
service GroupService {
  // Get group within the user's organization
  rpc GetGroup(GetGroupRequest) returns (GetGroupResponse) {
    option (google.api.http) = {
      get: "/authentication/groups/v1/{group_rid}"
      response_body: "group"
    };
  }

  // Get batch of groups within the user's organization
  rpc GetGroups(GetGroupsRequest) returns (GetGroupsResponse) {
    option (google.api.http) = {
      post: "/authentication/groups/v1/batch-get"
      body: "group_rids"
      response_body: "groups"
    };
  }

  // Gets a group by its ID within the user's organization
  rpc GetGroupById(GetGroupByIdRequest) returns (GetGroupByIdResponse) {
    option (google.api.http) = {
      get: "/authentication/groups/v1/id/{group_id}"
      response_body: "group"
    };
  }

  // buf:lint:ignore RPC_REQUEST_STANDARD_NAME
  // Updates the metadata of a group
  rpc UpdateGroupMetadata(UpdateGroupMetadataRequestWrapper) returns (UpdateGroupMetadataResponse) {
    option (google.api.http) = {
      put: "/authentication/groups/v1/{group_rid}/metadata"
      body: "request"
      response_body: "group"
    };
  }

  // Searches all groups in the user's organization using given query
  rpc SearchGroups(SearchGroupsRequest) returns (SearchGroupsResponse) {
    option (google.api.http) = {
      post: "/authentication/groups/v1/search"
      response_body: "*"
      body: "*"
    };
  }

  // Org-scoped upsert.
  //
  // Creates a new group if Group.rid is empty in the request (server assigns the rid).
  // Both Group.org_rid and Group.group_id are required on create, and the new group_id
  // must be unique within the organization.
  //
  // Updates an existing group if Group.rid is present in the request.
  // Group.org_rid and Group.group_id are immutable: when set on the update request they
  // must match the existing values, but they may be omitted to leave them unchanged.
  // Fields in the group message marked as OUTPUT_ONLY are ignored when present in the input request.
  // All other mutable fields are fully overwritten, and will be cleared if not set in the request.
  //
  // Throws HTTP 400 / gRPC 3 if a supplied immutable field does not match the existing group.
  // Throws HTTP 404 / gRPC 5 if the group identified by Group.rid for an update does not exist.
  // Throws HTTP 409 / gRPC 6 if a create would result in duplicate group_ids within the organization.
  // Throws HTTP 403 / gRPC 7 if the caller is not authorized.
  rpc CreateOrUpdateGroup(CreateOrUpdateGroupRequest) returns (CreateOrUpdateGroupResponse) {
    option (google.api.http) = {
      post: "/authentication/groups/v1"
      body: "*"
      response_body: "group"
    };
  }
}
