Skip to content

Commit bf81d61

Browse files
authored
Add permissions check in josh-filter (#503)
* Add permissions check in josh-filter * Add ACL toml file * Make the repo and users keys in ACL * Add access groups * Move to YAML * Make permissions tests a folder
1 parent 3c9ce5e commit bf81d61

File tree

12 files changed

+1059
-141
lines changed

12 files changed

+1059
-141
lines changed

josh-proxy/src/bin/josh-proxy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ async fn do_filter(
295295

296296
let mut headref = headref;
297297

298-
josh::filter_refs(&transaction, filter, &from_to)?;
298+
josh::filter_refs(&transaction, filter, &from_to, josh::filter::empty())?;
299299
if headref == "HEAD" {
300300
headref = heads_map
301301
.read()?

src/bin/josh-filter.rs

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,41 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
101101
.arg(
102102
clap::Arg::with_name("check-permission")
103103
.long("check-permission")
104-
.short("c")
104+
.short("c"),
105+
)
106+
.arg(clap::Arg::with_name("missing-permission").long("missing-permission"))
107+
.arg(
108+
clap::Arg::with_name("whitelist")
109+
.long("whitelist")
110+
.short("w")
111+
.takes_value(true),
112+
)
113+
.arg(
114+
clap::Arg::with_name("blacklist")
115+
.long("blacklist")
116+
.short("b")
117+
.takes_value(true),
118+
)
119+
.arg(
120+
clap::Arg::with_name("users")
121+
.long("users")
122+
.takes_value(true),
123+
)
124+
.arg(
125+
clap::Arg::with_name("groups")
126+
.long("groups")
127+
.takes_value(true),
128+
)
129+
.arg(
130+
clap::Arg::with_name("user")
131+
.long("user")
132+
.short("u")
133+
.takes_value(true),
134+
)
135+
.arg(
136+
clap::Arg::with_name("repo")
137+
.long("repo")
138+
.short("r")
105139
.takes_value(true),
106140
)
107141
.arg(clap::Arg::with_name("version").long("version").short("v"))
@@ -183,6 +217,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
183217
josh::filter::parse(&i)?,
184218
input_ref,
185219
"refs/JOSH_TMP",
220+
josh::filter::empty(),
186221
)?;
187222
}
188223
}
@@ -193,12 +228,6 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
193228
let target = update_target;
194229

195230
let reverse = args.is_present("reverse");
196-
let check_permissions = args.is_present("check-permission");
197-
198-
if check_permissions {
199-
filterobj = josh::filter::chain(josh::filter::parse(":PATHS")?, filterobj);
200-
filterobj = josh::filter::chain(filterobj, josh::filter::parse(":FOLD")?);
201-
}
202231

203232
let t = if reverse {
204233
"refs/JOSH_TMP".to_owned()
@@ -213,21 +242,49 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
213242
.unwrap()
214243
.to_string();
215244

216-
josh::filter_ref(&transaction, filterobj, &src, &t)?;
217-
218-
let mut all_paths = vec![];
219-
245+
let check_permissions = args.is_present("check-permission");
246+
let mut permissions_filter = josh::filter::empty();
220247
if check_permissions {
221-
let result_tree = repo.find_reference(&t)?.peel_to_tree()?;
248+
let whitelist;
249+
let blacklist;
250+
if args.is_present("users")
251+
&& args.is_present("groups")
252+
&& args.is_present("user")
253+
&& args.is_present("repo")
254+
{
255+
let users = args.value_of("users").unwrap();
256+
let groups = args.value_of("groups").unwrap();
257+
let user = args.value_of("user").unwrap();
258+
let repo = args.value_of("repo").unwrap();
259+
260+
let acl = josh::get_acl(users, groups, user, repo)?;
261+
whitelist = acl.0;
262+
blacklist = acl.1;
263+
} else {
264+
whitelist = match args.value_of("whitelist") {
265+
Some(s) => josh::filter::parse(s)?,
266+
_ => josh::filter::nop(),
267+
};
268+
blacklist = match args.value_of("blacklist") {
269+
Some(s) => josh::filter::parse(s)?,
270+
_ => josh::filter::empty(),
271+
};
272+
}
273+
permissions_filter = josh::filter::make_permissions_filter(filterobj, whitelist, blacklist)
274+
}
222275

223-
result_tree.walk(git2::TreeWalkMode::PreOrder, |_, entry| {
224-
let name = entry.name().unwrap();
225-
if name.starts_with("JOSH_ORIG_PATH_") {
226-
let pathname = josh::from_ns(&name.replacen("JOSH_ORIG_PATH_", "", 1));
227-
all_paths.push(pathname);
228-
}
229-
git2::TreeWalkResult::Ok
230-
})?;
276+
let missing_permissions = args.is_present("missing-permission");
277+
if missing_permissions {
278+
filterobj = permissions_filter;
279+
permissions_filter = josh::filter::empty();
280+
}
281+
282+
let updated_refs = josh::filter_ref(&transaction, filterobj, &src, &t, permissions_filter)?;
283+
if args.value_of("update") != Some("FILTERED_HEAD") && updated_refs == 0 {
284+
println!(
285+
"Warning: reference {} wasn't updated",
286+
args.value_of("update").unwrap()
287+
);
231288
}
232289

233290
#[cfg(feature = "search")]
@@ -264,39 +321,6 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
264321
/* println!("\n Search took {:?}", duration); */
265322
}
266323

267-
let mut dedup = vec![];
268-
269-
for w in all_paths.as_slice().windows(2) {
270-
if let [a, b, ..] = w {
271-
if !b.starts_with(a) {
272-
dedup.push(a.to_owned());
273-
}
274-
}
275-
}
276-
277-
let dedup = all_paths;
278-
279-
let options = glob::MatchOptions {
280-
case_sensitive: true,
281-
require_literal_separator: true,
282-
require_literal_leading_dot: true,
283-
};
284-
285-
if let Some(cp) = args.value_of("check-permission") {
286-
let pattern = glob::Pattern::new(cp)?;
287-
288-
let mut allowed = !dedup.is_empty();
289-
for d in dedup.iter() {
290-
let d = std::path::PathBuf::from(d);
291-
let m = pattern.matches_path_with(&d, options);
292-
if !m {
293-
allowed = false;
294-
println!("missing permission for: {:?}", &d);
295-
}
296-
}
297-
println!("Allowed = {:?}", allowed);
298-
}
299-
300324
if reverse {
301325
let new = repo.revparse_single(target).unwrap().id();
302326
let old = repo.revparse_single("JOSH_TMP").unwrap().id();

src/filter/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ impl std::fmt::Debug for Filter {
2929
return to_op(*self).fmt(f);
3030
}
3131
}
32+
3233
pub fn nop() -> Filter {
3334
to_filter(Op::Nop)
3435
}
3536

37+
pub fn empty() -> Filter {
38+
to_filter(Op::Empty)
39+
}
40+
3641
fn to_filter(op: Op) -> Filter {
3742
let s = format!("{:?}", op);
3843
let f = Filter(
@@ -767,6 +772,20 @@ fn compute_warnings2<'a>(
767772
warnings
768773
}
769774

775+
pub fn make_permissions_filter(filter: Filter, whitelist: Filter, blacklist: Filter) -> Filter {
776+
rs_tracing::trace_scoped!("make_permissions_filter");
777+
778+
let filter = chain(to_filter(Op::Paths), filter);
779+
let filter = chain(filter, to_filter(Op::Invert));
780+
let filter = chain(
781+
filter,
782+
compose(blacklist, to_filter(Op::Subtract(nop(), whitelist))),
783+
);
784+
let filter = opt::optimize(filter);
785+
786+
return filter;
787+
}
788+
770789
#[cfg(test)]
771790
mod tests {
772791
use super::*;

src/housekeeping.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ pub fn refresh_known_filters(
227227
upstream_repo,
228228
);
229229

230-
updated_count += filter_refs(&t, filter::parse(filter_spec)?, &refs)?;
230+
updated_count += filter_refs(&t, filter::parse(filter_spec)?, &refs, filter::empty())?;
231231
}
232232
info!("updated {} refs for {:?}", updated_count, upstream_repo);
233233
}

src/lib.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ extern crate pest_derive;
3535
#[macro_use]
3636
extern crate serde_json;
3737

38+
use std::collections::HashMap;
39+
use tracing;
40+
3841
pub mod cache;
3942
pub mod filter;
4043
pub mod graphql;
@@ -166,12 +169,34 @@ pub fn filter_ref(
166169
filterobj: filter::Filter,
167170
from_refsname: &str,
168171
to_refname: &str,
172+
permissions: filter::Filter,
169173
) -> JoshResult<usize> {
170174
let mut updated_count = 0;
171175
if let Ok(reference) = transaction.repo().revparse_single(from_refsname) {
172176
let original_commit = reference.peel_to_commit()?;
173177
let oid = original_commit.id();
174178

179+
let perms_commit = if let Some(s) = transaction.get_ref(permissions, oid) {
180+
s
181+
} else {
182+
tracing::trace!("apply_to_commit (permissions)");
183+
184+
filter::apply_to_commit(permissions, &original_commit, &transaction)?
185+
};
186+
187+
if perms_commit != git2::Oid::zero() {
188+
let perms_commit = transaction.repo().find_commit(perms_commit)?;
189+
if !perms_commit.tree()?.is_empty() || perms_commit.parents().len() > 0 {
190+
tracing::event!(
191+
tracing::Level::WARN,
192+
msg = "filter_refs: missing permissions for ref",
193+
warn = true,
194+
reference = from_refsname,
195+
);
196+
return Err(josh_error("missing permissions for ref"));
197+
}
198+
}
199+
175200
let filter_commit = if let Some(s) = transaction.get_ref(filterobj, oid) {
176201
s
177202
} else {
@@ -226,6 +251,7 @@ pub fn filter_refs(
226251
transaction: &cache::Transaction,
227252
filterobj: filter::Filter,
228253
refs: &[(String, String)],
254+
permissions: filter::Filter,
229255
) -> JoshResult<usize> {
230256
rs_tracing::trace_scoped!("filter_refs", "spec": filter::spec(filterobj));
231257
let s = tracing::Span::current();
@@ -235,7 +261,7 @@ pub fn filter_refs(
235261

236262
let mut updated_count = 0;
237263
for (k, v) in refs {
238-
updated_count += ok_or!(filter_ref(transaction, filterobj, k, v), {
264+
updated_count += ok_or!(filter_ref(&transaction, filterobj, &k, &v, permissions), {
239265
tracing::event!(
240266
tracing::Level::WARN,
241267
msg = "filter_refs: Can't filter reference",
@@ -275,3 +301,64 @@ pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
275301
}
276302
ret
277303
}
304+
305+
type Users = HashMap<String, User>;
306+
307+
#[derive(Debug, serde::Deserialize)]
308+
struct User {
309+
pub groups: toml::value::Array,
310+
}
311+
312+
type Groups = HashMap<String, HashMap<String, Group>>;
313+
#[derive(Debug, serde::Deserialize)]
314+
struct Group {
315+
pub whitelist: String,
316+
pub blacklist: String,
317+
}
318+
319+
pub fn get_acl(
320+
users: &str,
321+
groups: &str,
322+
user: &str,
323+
repo: &str,
324+
) -> JoshResult<(filter::Filter, filter::Filter)> {
325+
let users =
326+
std::fs::read_to_string(users).map_err(|_| josh_error("failed to read users file"))?;
327+
let users: Users = serde_yaml::from_str(&users)
328+
.map_err(|err| josh_error(format!("failed to parse users file: {}", err).as_str()))?;
329+
let groups =
330+
std::fs::read_to_string(groups).map_err(|_| josh_error("failed to read groups file"))?;
331+
let groups: Groups = serde_yaml::from_str(&groups)
332+
.map_err(|err| josh_error(format!("failed to parse groups file: {}", err).as_str()))?;
333+
334+
return users
335+
.get(user)
336+
.and_then(|u| {
337+
let mut whitelist = filter::empty();
338+
let mut blacklist = filter::empty();
339+
for g in &u.groups {
340+
let lists = groups.get(repo).and_then(|repo| {
341+
repo.get(g.as_str()?).and_then(|group| {
342+
let w = filter::parse(&group.whitelist);
343+
let b = filter::parse(&group.blacklist);
344+
Some((w, b))
345+
})
346+
})?;
347+
if let Err(e) = lists.0 {
348+
return Some(Err(JoshError(format!("Error parsing whitelist: {}", e))));
349+
}
350+
if let Err(e) = lists.1 {
351+
return Some(Err(JoshError(format!("Error parsing blacklist: {}", e))));
352+
}
353+
if let Ok(w) = lists.0 {
354+
whitelist = filter::compose(whitelist, w);
355+
}
356+
if let Ok(b) = lists.1 {
357+
blacklist = filter::compose(blacklist, b);
358+
}
359+
}
360+
println!("w: {:?}, b: {:?}", whitelist, blacklist);
361+
Some(Ok((whitelist, blacklist)))
362+
})
363+
.unwrap_or(Ok((filter::empty(), filter::nop())));
364+
}

tests/filter/empty_head.t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
$ git commit -m "add file5" 1> /dev/null
3636

3737
$ josh-filter -s :/sub2 master --update refs/josh/filter/master
38+
Warning: reference refs/josh/filter/master wasn't updated
3839
[2] :/sub1
3940
[2] :/sub2
4041
$ git log --graph --pretty=%s josh/filter/master

tests/filter/infofile.t

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* add file1
3131

3232
$ josh-filter -s c=:/sub1 master --update refs/josh/filter/master
33+
Warning: reference refs/josh/filter/master wasn't updated
3334
[2] :/sub1
3435
[2] :prefix=c
3536
$ git log --graph --pretty=%s josh/filter/master
@@ -48,6 +49,7 @@
4849
$ git commit -m "add file5" 1> /dev/null
4950

5051
$ josh-filter -s c=:/sub2 master --update refs/josh/filter/master
52+
Warning: reference refs/josh/filter/master wasn't updated
5153
[2] :/sub1
5254
[2] :/sub2
5355
[3] :prefix=c

0 commit comments

Comments
 (0)