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
// Copyright 2018 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

//! App revocation functions

use super::{AuthError, AuthFuture};
use crate::access_container;
use crate::client::AuthClient;
use crate::config::{self, AppInfo, RevocationQueue};
use futures::future::{self, Either, Loop};
use futures::Future;
use log::trace;
use safe_core::recoverable_apis;
use safe_core::{client::AuthActions, Client, CoreError, FutureExt, MDataInfo};
use safe_core::{err, ok};
use safe_nd::{Error as SndError, PublicKey};
use std::collections::HashMap;

type Containers = HashMap<String, MDataInfo>;

/// Revoke app access using a revocation queue.
pub fn revoke_app(client: &AuthClient, app_id: &str) -> Box<AuthFuture<()>> {
    let app_id = app_id.to_string();
    let client = client.clone();
    let c2 = client.clone();

    config::get_app_revocation_queue(&client)
        .and_then(move |(version, queue)| {
            config::push_to_app_revocation_queue(
                &client,
                queue,
                config::next_version(version),
                &app_id,
            )
        })
        .and_then(move |(version, queue)| flush_app_revocation_queue_impl(&c2, queue, version + 1))
        .into_box()
}

/// Revoke all apps currently in the revocation queue.
pub fn flush_app_revocation_queue(client: &AuthClient) -> Box<AuthFuture<()>> {
    let client = client.clone();

    config::get_app_revocation_queue(&client)
        .and_then(move |(version, queue)| {
            if let Some(version) = version {
                flush_app_revocation_queue_impl(&client, queue, version + 1)
            } else {
                future::ok(()).into_box()
            }
        })
        .into_box()
}

// Try to revoke all apps in the revocation queue. If app revocation results in an error, move the
// app to the back of the queue. Keep track of failed apps and if one fails again after moving to
// the end of the queue, return its error. In other words, we revoke all the apps that we can and
// return an error for the first app that fails twice.
//
// The exception to this is if we encounter a `SymmetricDecipherFailure` error, which we know is
// irrecoverable, so in this case we remove the app from the queue and return an error immediately.
fn flush_app_revocation_queue_impl(
    client: &AuthClient,
    queue: RevocationQueue,
    version: u64,
) -> Box<AuthFuture<()>> {
    let client = client.clone();
    let moved_apps = Vec::new();

    future::loop_fn(
        (queue, version, moved_apps),
        move |(queue, version, mut moved_apps)| {
            let c2 = client.clone();
            let c3 = client.clone();

            if let Some(app_id) = queue.front().cloned() {
                let f = revoke_single_app(&c2, &app_id)
                    .then(move |result| match result {
                        Ok(_) => {
                            config::remove_from_app_revocation_queue(&c3, queue, version, &app_id)
                                .map(|(version, queue)| (version, queue, moved_apps))
                                .into_box()
                        }
                        Err(AuthError::CoreError(CoreError::SymmetricDecipherFailure)) => {
                            // The app entry can't be decrypted. No way to revoke app, so just
                            // remove it from the queue and return an error.
                            config::remove_from_app_revocation_queue(&c3, queue, version, &app_id)
                                .and_then(|_| {
                                    err!(AuthError::CoreError(CoreError::SymmetricDecipherFailure))
                                })
                                .into_box()
                        }
                        Err(error) => {
                            if moved_apps.contains(&app_id) {
                                // If this app has already been moved to the back of the queue,
                                // return the error.
                                err!(error)
                            } else {
                                // Move the app to the end of the queue.
                                moved_apps.push(app_id.clone());
                                config::repush_to_app_revocation_queue(&c3, queue, version, &app_id)
                                    .map(|(version, queue)| (version, queue, moved_apps))
                                    .into_box()
                            }
                        }
                    })
                    .and_then(move |(version, queue, moved_apps)| {
                        Ok(Loop::Continue((queue, version + 1, moved_apps)))
                    });
                Either::A(f)
            } else {
                Either::B(future::ok(Loop::Break(())))
            }
        },
    )
    .into_box()
}

// Revoke access for a single app
fn revoke_single_app(client: &AuthClient, app_id: &str) -> Box<AuthFuture<()>> {
    trace!("Revoking app with ID {}...", app_id);

    let c2 = client.clone();
    let c3 = client.clone();
    let c4 = client.clone();

    // 1. Delete the app key from the Client Handlers
    // 2. Remove the app key from containers permissions
    // 3. Refresh the containers info from the user's root dir (as the access
    //    container entry is not updated with the new keys info - so we have to
    //    make sure that we use correct encryption keys if the previous revoke
    //    attempt has failed)
    // 4. Remove the revoked app from the access container
    config::get_app(client, app_id)
        .and_then(move |app| delete_app_auth_key(&c2, app.keys.public_key()).map(move |_| app))
        .and_then(move |app| {
            access_container::fetch_entry(&c3, &app.info.id, app.keys.clone()).and_then(
                move |(version, ac_entry)| {
                    if let Some(ac_entry) = ac_entry {
                        let containers: Containers = ac_entry
                            .into_iter()
                            .map(|(name, (mdata_info, _))| (name, mdata_info))
                            .collect();

                        clear_from_access_container_entry(&c4, app, version, containers)
                    } else {
                        // If the access container entry was not found, the entry must have been
                        // deleted with the app having stayed on the revocation queue.
                        ok!(())
                    }
                },
            )
        })
        .into_box()
}

// Delete the app auth key from the Maid Manager - this prevents the app from
// performing any more mutations.
fn delete_app_auth_key(client: &AuthClient, key: PublicKey) -> Box<AuthFuture<()>> {
    let client = client.clone();

    client
        .list_auth_keys_and_version()
        .and_then(move |(listed_keys, version)| {
            if listed_keys.contains_key(&key) {
                client.del_auth_key(key, version + 1)
            } else {
                // The key has been removed already
                ok!(())
            }
        })
        .or_else(|error| match error {
            CoreError::DataError(SndError::NoSuchKey) => Ok(()),
            error => Err(AuthError::from(error)),
        })
        .into_box()
}

fn clear_from_access_container_entry(
    client: &AuthClient,
    app: AppInfo,
    ac_entry_version: u64,
    containers: Containers,
) -> Box<AuthFuture<()>> {
    let c2 = client.clone();

    revoke_container_perms(client, &containers, app.keys.public_key())
        .map(move |_| (app, ac_entry_version))
        .and_then(move |(app, version)| {
            access_container::delete_entry(&c2, &app.info.id, &app.keys, version + 1)
        })
        .into_box()
}

// Revoke containers permissions
fn revoke_container_perms(
    client: &AuthClient,
    containers: &Containers,
    pk: PublicKey,
) -> Box<AuthFuture<()>> {
    let reqs: Vec<_> = containers
        .values()
        .map(|mdata_info| {
            let mdata_info = mdata_info.clone();
            let c2 = client.clone();

            client
                .clone()
                .get_mdata_version(*mdata_info.address())
                .and_then(move |version| {
                    recoverable_apis::del_mdata_user_permissions(
                        &c2,
                        *mdata_info.address(),
                        pk,
                        version + 1,
                    )
                })
                .map_err(From::from)
        })
        .collect();

    future::join_all(reqs).map(move |_| ()).into_box()
}