package caldav

  1. Overview
  2. Docs

Source file properties.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
module Xml = Webdav_xml

let prop_version = [ Xml.pcdata "2" ]

module PairMap = Map.Make (struct
    type t = string * string
    let compare (a1, a2) (b1, b2) = match String.compare a1 b1 with
      | 0 -> String.compare a2 b2
      | x -> x
end)

open Sexplib.Conv
type property = Xml.attribute list * Xml.tree list [@@deriving sexp]

type t = property PairMap.t

type property_list = ((string * string) * property) list [@@deriving sexp]

let to_sexp t =
  let bindings = PairMap.bindings t in
  sexp_of_property_list bindings

let of_sexp now s =
  let bindings = property_list_of_sexp s in
  let map =
    List.fold_left (fun map (k, v) -> PairMap.add k v map) PairMap.empty bindings
  in
  match PairMap.find_opt (Xml.robur_ns, "prop_version") map with
  | Some ([], [ Xml.Pcdata n ]) ->
    begin match int_of_string n with
      | exception Failure _ ->
        Logs.warn (fun m -> m "couldn't parse version"); map
      | 0 | 1 ->
        let current = [], [ Xml.Pcdata (Ptime.to_rfc3339 now) ] in
        (* version 0 and 1 didn't write the lastmodified *)
        (* for directories, we use the current timestamp,
           for files the creationdate (which is always updated) *)
        let ts = match PairMap.find_opt (Xml.dav_ns, "getcontenttype") map with
          | Some ([], [ Xml.Pcdata ct ]) when ct = "text/directory" -> current
          | _ ->
            begin match PairMap.find_opt (Xml.dav_ns, "creationdate") map with
              | None -> Logs.warn (fun m -> m "map without creationdate"); current
              | Some v -> v
            end
        in
        PairMap.add (Xml.robur_ns, "prop_version") ([], prop_version)
          (PairMap.add (Xml.dav_ns, "getlastmodified") ts map)
      | _ -> map
    end
  | _ -> (* shouldn't happen *)
    Logs.warn (fun m -> m "property map without version"); map

(* not safe *)
let unsafe_find = PairMap.find_opt

let unsafe_add = PairMap.add

let unsafe_remove = PairMap.remove

(* public and ok *)
let empty = PairMap.empty

(* internal *)
let keys m = List.map fst (PairMap.bindings m)

(* public and ok *)
let count = PairMap.cardinal

let not_returned_by_allprop = [
  (Xml.robur_ns, "prop_version");
  (Xml.dav_ns, "owner");
  (Xml.dav_ns, "group");
  (Xml.dav_ns, "supported-privilege-set");
  (Xml.dav_ns, "current-user-privilege-set");
  (Xml.dav_ns, "acl");
  (Xml.dav_ns, "acl-restrictions");
  (Xml.dav_ns, "inherited-acl-set");
  (Xml.dav_ns, "principal-collection-set");
  (Xml.caldav_ns, "calendar-description");
  (Xml.caldav_ns, "calendar-timezone");
  (Xml.caldav_ns, "supported-calendar-component-set");
  (Xml.caldav_ns, "supported-calendar-data");
  (Xml.caldav_ns, "max-resource-size");
  (Xml.caldav_ns, "min-date-time");
  (Xml.caldav_ns, "max-date-time");
  (Xml.caldav_ns, "max-instances");
  (Xml.caldav_ns, "max-attendees-per-instance");
  (Xml.caldav_ns, "calendar-home-set");
  (Xml.caldav_ns, "supported-collation-set");
  (Xml.robur_ns, "password");
  (Xml.robur_ns, "salt");
]

let write_protected = [
  (Xml.robur_ns, "prop_version");
  (Xml.dav_ns, "principal-URL");
  (Xml.dav_ns, "group-membership");
  (Xml.dav_ns, "resourcetype");
  (Xml.dav_ns, "current-user-principal");
  (Xml.dav_ns, "current-user-privilege-set");
  (Xml.dav_ns, "content-length");
  (Xml.dav_ns, "etag");
]

let computed_properties = [
  (Xml.dav_ns, "current-user-privilege-set") ;
  (Xml.dav_ns, "current-user-principal")
]

(* assume that it is safe, should call can_write_prop *)
(* TODO check `Write_acl if writing an ACL property *)
let patch ?(is_mkcol = false) props_for_resource updates =
  (* if an update did not apply, m will be None! *)
  let xml (ns, n) = [ Xml.node ~ns n [] ] in
  let apply (props_for_resource, propstats) update = match props_for_resource, update with
    | None, `Set (_, k, _) -> None, (`Failed_dependency, xml k) :: propstats
    | None, `Remove k   -> None, (`Failed_dependency, xml k) :: propstats
    | Some props_for_resource', `Set (a, k, v) ->
      if List.mem k write_protected && not (is_mkcol && k = (Xml.dav_ns, "resourcetype"))
      then None, (`Forbidden, xml k) :: propstats
      else
        let props_for_resource'' = unsafe_add k (a, v) props_for_resource' in
        (Some props_for_resource'', (`OK, xml k) :: propstats)
    | Some props_for_resource', `Remove k ->
      if List.mem k write_protected
      then None, (`Forbidden, xml k) :: propstats
      else
        let props_for_resource'' = unsafe_remove k props_for_resource' in
        Some props_for_resource'', (`OK, xml k) :: propstats
  in
  match List.fold_left apply (Some props_for_resource, []) updates with
  | Some props_for_resource', xs -> Some props_for_resource', xs
  | None, xs ->
    (* some update did not apply -> tree: None *)
    let ok_to_failed (s, k) =
      ((match s with
          | `OK -> `Failed_dependency
          | x -> x), k)
    in
    None, List.map ok_to_failed xs

(* housekeeping *)
let to_trees m =
  PairMap.fold (fun (ns, k) (a, v) acc ->
    Xml.node ~ns ~a k v :: acc) m []

(* housekeeping *)
let to_string m =
  let c = to_trees m in
  Xml.tree_to_string (Xml.dav_node "prop" c)

(* housekeeping *)
let pp ppf t = Fmt.string ppf @@ to_string t

(* housekeeping *)
let equal a b = String.equal (to_string a) (to_string b)

(* creates property map for file, only needs to check `Bind in parent, done by webmachine *)
let create ?(initial_props = []) ?(content_type = "text/html") ?(language = "en") ?(resourcetype = []) acl timestamp length filename =
  let filename = if filename = "" then "hinz und kunz" else filename in
  let timestamp' = Ptime.to_rfc3339 timestamp in
  let propmap =
    unsafe_add (Xml.robur_ns, "prop_version") ([], prop_version) @@
    unsafe_add (Xml.dav_ns, "acl") ([], List.map Xml.ace_to_xml acl) @@
    unsafe_add (Xml.dav_ns, "creationdate") ([], [ Xml.Pcdata timestamp' ]) @@
    unsafe_add (Xml.dav_ns, "displayname") ([], [ Xml.Pcdata filename ]) @@
    unsafe_add (Xml.dav_ns, "getcontentlanguage") ([], [ Xml.Pcdata language ]) @@
    unsafe_add (Xml.dav_ns, "getcontenttype") ([], [ Xml.Pcdata content_type ]) @@
    unsafe_add (Xml.dav_ns, "getcontentlength") ([], [ Xml.Pcdata (string_of_int length) ]) @@
    unsafe_add (Xml.dav_ns, "getlastmodified") ([], [ Xml.Pcdata timestamp' ]) @@
    (* unsafe_add "lockdiscovery" *)
    unsafe_add (Xml.dav_ns, "resourcetype") ([], resourcetype) empty
    (* unsafe_add "supportedlock" *)
  in
  List.fold_left (fun p (k, v) -> unsafe_add k v p) propmap initial_props

(* creates property map for directory *)
let create_dir ?initial_props ?(resourcetype = []) acl timestamp dirname =
  create ?initial_props ~content_type:"text/directory"
    ~resourcetype:(Xml.dav_node "collection" [] :: resourcetype)
    acl timestamp 0 dirname

(* housekeeping *)
let from_tree = function
  | Xml.Node (_, "prop", _, children) ->
    List.fold_left (fun m c -> match c with
        | Xml.Node (ns, k, a, v) -> unsafe_add (ns, k) (a, v) m
        | Xml.Pcdata _ -> assert false)
      empty children
  | _ -> assert false

(* TODO groups only one level deep right now *)
(* TODO belongs elsewhere? *)
(* outputs identities for a single user *)
let identities userprops =
  let url = function
    | Xml.Node (_, "href", _, [ Xml.Pcdata url ]) -> [ Uri.of_string url ]
    | _ -> []
  in
  let urls n = List.flatten (List.map url n) in
  match
    unsafe_find (Xml.dav_ns, "principal-URL") userprops,
    unsafe_find (Xml.dav_ns, "group-membership") userprops
  with
  | None, _ -> []
  | Some (_, principal), Some (_, groups) -> urls principal @ urls groups
  | Some (_, principal), None -> urls principal

let privileges ~auth_user_props resource_props =
  let aces = match unsafe_find (Xml.dav_ns, "acl") resource_props with
    | None -> []
    | Some (_, aces) -> aces
  in
  Privileges.list ~identities:(identities auth_user_props) aces

let inherited_acls ~auth_user_props resource_props =
  let aces = match unsafe_find (Xml.dav_ns, "acl") resource_props with
    | None -> []
    | Some (_, aces) -> aces
  in
  Logs.debug (fun m -> m "inherited aces size %d" (List.length aces));
  let inherited = Privileges.inherited_acls ~identities:(identities auth_user_props) aces in
  Logs.debug (fun m -> m "inherited size %d" (List.length inherited));
  inherited

(* helper computing "current-user-privilege-set", not public *)
let current_user_privilege_set ~auth_user_props map =
  let make_node p = Xml.dav_node "privilege" [ Xml.priv_to_xml p ] in
  let privileges = privileges ~auth_user_props map in
  let uniq =
    (* workaround for Firefox OS which doesn't understand <privilege><all/></privilege> *)
    if List.mem `All privileges
    then [ `Read ; `Write ; `Read_current_user_privilege_set ; `Write_content ; `Write_properties ; `Bind ; `Unbind ; `All ]
    else List.sort_uniq compare privileges
  in
  Some ([], (List.map make_node uniq))

(* checks nothing, computes current-user-principal, helper function *)
let current_user_principal props =
  match unsafe_find (Xml.dav_ns, "principal-URL") props with
  | None -> Some ([], [ Xml.dav_node "unauthenticated" [] ])
  | Some url -> Some url

(* checks nothing, computes properties, should be visible? but requires auth_user_props *)
let get_prop auth_user_props m = function
  | ns, "current-user-privilege-set" when ns = Xml.dav_ns -> current_user_privilege_set ~auth_user_props m
  | ns, "current-user-principal" when ns = Xml.dav_ns -> current_user_principal auth_user_props
  | fqname -> unsafe_find fqname m

let find ~auth_user_props ~resource_props property_fqname =
  let privileges = privileges ~auth_user_props resource_props in
  if Privileges.can_read_prop property_fqname privileges
  then match get_prop auth_user_props resource_props property_fqname with
    | None -> Error `Not_found
    | Some v -> Ok v
  else Error `Forbidden

let transform_lastmodified = function
  | None -> None
  | Some (attrs, [ Xml.Pcdata str ]) ->
    Some (attrs, [ Xml.Pcdata (Xml.rfc3339_date_to_http_date str) ])
  | Some _ -> assert false

(* checks sufficient privileges for "current-user-privilege-set" and "read-acl" via can_read_prop *)
let find_many ~auth_user_props ~resource_props property_names =
  let resource_props = PairMap.update (Xml.dav_ns, "getlastmodified") transform_lastmodified resource_props in
  let props = List.map (find ~auth_user_props ~resource_props) property_names in
  let results = List.map2 (fun (ns, name) p -> p, match p with
    | Ok (a, c) -> Xml.node ~ns ~a name c
    | Error _   -> Xml.node ~ns name []) property_names props
  in
  (* group by return code *)
  let found, rest = List.partition (function | Ok _, _ -> true | _ -> false) results in
  let not_found, forbidden = List.partition (function | Error `Not_found, _ -> true | Error `Forbidden, _ -> false | Ok _, _ -> assert false) rest in
  let apply_tag tag l = if l = [] then [] else [ tag, List.map snd l ] in
  apply_tag `OK found @ apply_tag `Not_found not_found @ apply_tag `Forbidden forbidden

(* not safe, exposed, returns property names *)
let names m =
  List.map (fun (ns, k) -> Xml.node ~ns k []) @@
  computed_properties @ keys m

(* not really safe, but excludes from the not-returned-by-allprop list *)
let all m =
  let m' = PairMap.update (Xml.dav_ns, "getlastmodified") transform_lastmodified m in
  to_trees (List.fold_right unsafe_remove not_returned_by_allprop m')
OCaml

Innovation. Community. Security.