<commit_before>(ns cmr.access-control.api.routes
  "Defines the HTTP URL routes for the application."
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [compojure.core :refer :all]
            [ring.middleware.params :as params]
            [ring.middleware.nested-params :as nested-params]
            [ring.middleware.keyword-params :as keyword-params]
            [ring.middleware.json :as ring-json]
            [cheshire.core :as json]
            [cmr.common.concepts :as cc]
            [cmr.common.log :refer (debug info warn error)]
            [cmr.common.cache :as cache]
            [cmr.common.api.errors :as api-errors]
            [cmr.common.services.errors :as errors]
            [cmr.common.api.context :as context]
            [cmr.common.validations.core :as validation]
            [cmr.common.validations.json-schema :as js]
            [cmr.common.mime-types :as mt]
            [cmr.acl.core :as acl]
            [cmr.common-app.api.routes :as cr]
            [cmr.common-app.api-docs :as api-docs]
            [cmr.access-control.data.access-control-index :as index]
            [cmr.access-control.data.acl-schema :as acl-schema]
            [cmr.access-control.services.acl-service :as acl-service]
            [cmr.access-control.services.group-service :as group-service]
            [cmr.common.util :as util]
            [clojure.string :as str]))

;;; Utility Functions

(defn- validate-params
  "Throws a service error when any keys exist in params other than those in allowed-param-names."
  [params & allowed-param-names]
  (when-let [invalid-params (seq (remove (set allowed-param-names) (keys params)))]
    (errors/throw-service-errors :bad-request (for [param invalid-params]
                                                (format "Parameter [%s] was not recognized."
                                                        (name param))))))

(defn- validate-standard-params
  "Throws a service error if any parameters other than :token or :pretty are present."
  [params]
  (validate-params params :pretty :token))

(defn- api-response
  "Creates a successful response with the given data response"
  ([data]
   (api-response data true))
  ([data encode?]
   {:status 200
    :body (if encode? (json/generate-string data) data)
    :headers {"Content-Type" mt/json}}))

(defn- validate-content-type
  "Validates that content type sent is JSON"
  [headers]
  (mt/extract-header-mime-type #{mt/json} headers "content-type" true))

(defn- validate-json
  "Throws :bad-request service error if json-str fails to validate against given schema."
  [schema json-str]
  (when-let [errors (seq (js/validate-json schema json-str))]
    (errors/throw-service-errors :bad-request errors)))

;;; Group Schema and Params Utils

(def ^:private group-schema-structure
  "Schema for groups as json."
  {:type :object
   :additionalProperties false
   :properties {:name {:type :string :minLength 1 :maxLength 100}
                :provider_id {:type :string :minLength 1 :maxLength 50}
                :description {:type :string :minLength 1 :maxLength 255}
                :legacy_guid {:type :string :minLength 1 :maxLength 50}
                :members {:type :array :items {:type :string :minLength 1 :maxLength 100}}}
   :required [:name :description]})

(def ^:private group-schema
  "The JSON schema used to validate groups"
  (js/parse-json-schema group-schema-structure))

(def ^:private group-members-schema-structure
  "Schema defining list of usernames sent to add or remove members in a group"
  {:type :array :items {:type :string :minLength 1 :maxLength 50}})

(def ^:private group-members-schema
  "The JSON schema used to validate a list of group members"
  (js/parse-json-schema group-members-schema-structure))

(defn- validate-group-route-params
  "Same as validate-standard-params plus :group-id."
  [params]
  (validate-params params :pretty :token :group-id))

(defn- validate-group-json
  "Validates the group JSON string against the schema. Throws a service error if it is invalid."
  [json-str]
  (validate-json group-schema json-str))

(defn- validate-group-members-json
  "Validates the group mebers JSON string against the schema. Throws a service error if it is invalid."
  [json-str]
  (validate-json group-members-schema json-str))

;; Misc route validations

(defn- validate-get-permission-params
  "Throws service errors if any invalid params or values are found."
  [params]
  (validate-params params :system_object :concept_id :user_id :user_type)
  (let [{:keys [system_object concept_id user_id user_type]} params
        errors []
        errors (if (or (and (not (str/blank? system_object))
                            (seq concept_id))
                       (and (str/blank? system_object) (empty? concept_id)))
                 (conj errors "One of parameters [concept_id] or [system_object] are required.")
                 errors)
        errors (if system_object
                 (if (some #{system_object} acl-schema/system-object-targets)
                   errors
                   (conj errors (str "Parameter [system_object] must be one of: " (pr-str acl-schema/system-object-targets))))
                 errors)
        errors (reduce #(concat %1 (cc/concept-id-validation %2)) errors concept_id)
        errors (if-not (= 1 (count (remove str/blank? [user_id user_type])))
                 (conj errors "One of parameters [user_type] or [user_id] are required.")
                 errors)]
    (when (seq errors)
      (errors/throw-service-errors :bad-request errors))))

;;; Group Route Functions

(defn create-group
  "Processes a create group request."
  [context headers body]
  (validate-content-type headers)
  (validate-group-json body)
  (->> (json/parse-string body true)
       (util/map-keys->kebab-case)
       (group-service/create-group context)
       (util/map-keys->snake_case)
       api-response))

(defn get-group
  "Retrieves the group with the given concept-id."
  [context concept-id]
  (-> (group-service/get-group context concept-id)
      (util/map-keys->snake_case)
      api-response))
<commit_msg>CMR-2897 Refactors permission check parameter validations<commit_after>(ns cmr.access-control.api.routes
  "Defines the HTTP URL routes for the application."
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [compojure.core :refer :all]
            [ring.middleware.params :as params]
            [ring.middleware.nested-params :as nested-params]
            [ring.middleware.keyword-params :as keyword-params]
            [ring.middleware.json :as ring-json]
            [cheshire.core :as json]
            [cmr.common.concepts :as cc]
            [cmr.common.log :refer (debug info warn error)]
            [cmr.common.cache :as cache]
            [cmr.common.api.errors :as api-errors]
            [cmr.common.services.errors :as errors]
            [cmr.common.api.context :as context]
            [cmr.common.validations.core :as validation]
            [cmr.common.validations.json-schema :as js]
            [cmr.common.mime-types :as mt]
            [cmr.acl.core :as acl]
            [cmr.common-app.api.routes :as cr]
            [cmr.common-app.api-docs :as api-docs]
            [cmr.access-control.data.access-control-index :as index]
            [cmr.access-control.data.acl-schema :as acl-schema]
            [cmr.access-control.services.acl-service :as acl-service]
            [cmr.access-control.services.group-service :as group-service]
            [cmr.common.util :as util]
            [clojure.string :as str]))

;;; Utility Functions

(defn- validate-params
  "Throws a service error when any keys exist in params other than those in allowed-param-names."
  [params & allowed-param-names]
  (when-let [invalid-params (seq (remove (set allowed-param-names) (keys params)))]
    (errors/throw-service-errors :bad-request (for [param invalid-params]
                                                (format "Parameter [%s] was not recognized."
                                                        (name param))))))

(defn- validate-standard-params
  "Throws a service error if any parameters other than :token or :pretty are present."
  [params]
  (validate-params params :pretty :token))

(defn- api-response
  "Creates a successful response with the given data response"
  ([data]
   (api-response data true))
  ([data encode?]
   {:status 200
    :body (if encode? (json/generate-string data) data)
    :headers {"Content-Type" mt/json}}))

(defn- validate-content-type
  "Validates that content type sent is JSON"
  [headers]
  (mt/extract-header-mime-type #{mt/json} headers "content-type" true))

(defn- validate-json
  "Throws :bad-request service error if json-str fails to validate against given schema."
  [schema json-str]
  (when-let [errors (seq (js/validate-json schema json-str))]
    (errors/throw-service-errors :bad-request errors)))

;;; Group Schema and Params Utils

(def ^:private group-schema-structure
  "Schema for groups as json."
  {:type :object
   :additionalProperties false
   :properties {:name {:type :string :minLength 1 :maxLength 100}
                :provider_id {:type :string :minLength 1 :maxLength 50}
                :description {:type :string :minLength 1 :maxLength 255}
                :legacy_guid {:type :string :minLength 1 :maxLength 50}
                :members {:type :array :items {:type :string :minLength 1 :maxLength 100}}}
   :required [:name :description]})

(def ^:private group-schema
  "The JSON schema used to validate groups"
  (js/parse-json-schema group-schema-structure))

(def ^:private group-members-schema-structure
  "Schema defining list of usernames sent to add or remove members in a group"
  {:type :array :items {:type :string :minLength 1 :maxLength 50}})

(def ^:private group-members-schema
  "The JSON schema used to validate a list of group members"
  (js/parse-json-schema group-members-schema-structure))

(defn- validate-group-route-params
  "Same as validate-standard-params plus :group-id."
  [params]
  (validate-params params :pretty :token :group-id))

(defn- validate-group-json
  "Validates the group JSON string against the schema. Throws a service error if it is invalid."
  [json-str]
  (validate-json group-schema json-str))

(defn- validate-group-members-json
  "Validates the group mebers JSON string against the schema. Throws a service error if it is invalid."
  [json-str]
  (validate-json group-members-schema json-str))

;; Misc route validations

(defn system_object-concept_id-validation
  "Validates system_object and concept_id parameter lookup permissions."
  [{:keys [system_object concept_id]}]
  (when (or (and (not (str/blank? system_object))
                 (seq concept_id))
            (and (str/blank? system_object) (empty? concept_id)))
    ["One of parameters [concept_id] or [system_object] are required."]))

(defn system_object-validation
  "Validates that system_object parameter has a valid value, if present."
  [{:keys [system_object]}]
  (when system_object
    (when-not (some #{system_object} acl-schema/system-object-targets)
      [(str "Parameter [system_object] must be one of: " (pr-str acl-schema/system-object-targets))])))

(defn concept_ids-validation
  "Validates that all values in the multi-valued concept_id param are valid concept IDs"
  [{:keys [concept_id]}]
  (mapcat cc/concept-id-validation concept_id))

(defn user_id-user_type-validation
  "Validates that only one of user_id or user_type are specified."
  [{:keys [user_id user_type]}]
  (if-not (= 1 (count (remove str/blank? [user_id user_type])))
    ["One of parameters [user_type] or [user_id] are required."]))

(def get-permissions-validations
  [system_object-concept_id-validation
   user_id-user_type-validation
   system_object-validation
   concept_ids-validation])

(defn- validate-get-permission-params
  "Throws service errors if any invalid params or values are found."
  [params]
  (validate-params params :system_object :concept_id :user_id :user_type)
  (when-let [errors (seq (mapcat #(% params) get-permissions-validations))]
    (errors/throw-service-errors :bad-request errors)))

;;; Group Route Functions

(defn create-group
  "Processes a create group request."
  [context headers body]
  (validate-content-type headers)
  (validate-group-json body)
  (->> (json/parse-string body true)
       (util/map-keys->kebab-case)
       (group-service/create-group context)
       (util/map-keys->snake_case)
       api-response))

(defn get-group
  "Retrieves the group with the given concept-id."
  [context concept-id]
  (-> (group-service/get-group context concept-id)
      (util/map-keys->snake_case)
      api-response))

{'fraction_non_alphanumeric': 0.11829220524872698, 'fraction_numerical': 0.0043869956913435175, 'mean_word_length': 2.85912938331318, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3518561', 'n_tokens_mistral': 3935, 'n_tokens_neox': 3812, 'n_words': 1134}