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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
// 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 authentication routines

use super::{AuthError, AuthFuture};
use crate::access_container;
use crate::access_container::update_container_perms;
use crate::app_container;
use crate::client::AuthClient;
use crate::config::{self, AppInfo, Apps};
use futures::future::{self, Either};
use futures::Future;
use log::trace;
use safe_core::client;
use safe_core::core_structs::{AccessContInfo, AccessContainerEntry, AppKeys};
use safe_core::ipc::req::{AuthReq, ContainerPermissions, Permission};
use safe_core::ipc::resp::AuthGranted;
use safe_core::{
    app_container_name, client::AuthActions, recoverable_apis, Client, FutureExt, MDataInfo,
};
use safe_core::{btree_set, err, fry, ok};
use safe_nd::AppPermissions;
use std::collections::HashMap;
use tiny_keccak::sha3_256;
use unwrap::unwrap;

/// Represents current app state
#[derive(Debug, Eq, PartialEq)]
pub enum AppState {
    /// Exists in the authenticator config, access container, and registered in MaidManagers
    Authenticated,
    /// Exists in the authenticator config but not in access container and MaidManagers
    Revoked,
    /// Doesn't exist in the authenticator config
    NotAuthenticated,
}

/// Return a current app state (`Authenticated` if it has an entry
/// in the config file AND the access container, `Revoked` if it has
/// an entry in the config but not in the access container, and `NotAuthenticated`
/// if it's not registered anywhere).
pub fn app_state(client: &AuthClient, apps: &Apps, app_id: &str) -> Box<AuthFuture<AppState>> {
    let app_id_hash = sha3_256(app_id.as_bytes());

    if let Some(app) = apps.get(&app_id_hash) {
        let app_keys = app.keys.clone();

        access_container::fetch_entry(client, app_id, app_keys)
            .then(move |res| {
                match res {
                    Ok((_version, Some(_))) => Ok(AppState::Authenticated),
                    Ok((_, None)) => {
                        // App is not in access container, so it is revoked
                        Ok(AppState::Revoked)
                    }
                    Err(e) => Err(e),
                }
            })
            .into_box()
    } else {
        ok!(AppState::NotAuthenticated)
    }
}

/// Check whether `permissions` has an app container entry for `app_id` and that all permissions are
/// set.
fn app_container_exists(permissions: &AccessContainerEntry, app_id: &str) -> bool {
    match permissions.get(&app_container_name(app_id)) {
        Some(&(_, ref access)) => {
            *access
                == btree_set![
                    Permission::Read,
                    Permission::Insert,
                    Permission::Update,
                    Permission::Delete,
                    Permission::ManagePermissions,
                ]
        }
        None => false,
    }
}

/// Insert info about the app's dedicated container into the access container entry
fn insert_app_container(
    mut permissions: AccessContainerEntry,
    app_id: &str,
    app_container_info: MDataInfo,
) -> AccessContainerEntry {
    let access = btree_set![
        Permission::Read,
        Permission::Insert,
        Permission::Update,
        Permission::Delete,
        Permission::ManagePermissions,
    ];
    let _ = permissions.insert(app_container_name(app_id), (app_container_info, access));
    permissions
}

fn update_access_container(
    client: &AuthClient,
    app: &AppInfo,
    permissions: AccessContainerEntry,
) -> Box<AuthFuture<()>> {
    let c2 = client.clone();

    let app_id = app.info.id.clone();
    let app_keys = app.keys.clone();

    trace!("Updating access container entry for app {}...", app_id);
    access_container::fetch_entry(client, &app_id, app_keys.clone())
        .then(move |res| {
            let version = match res {
                // Updating an existing entry
                Ok((version, Some(_))) => version + 1,
                // Adding a new access container entry
                Ok((_, None)) => 0,
                // Error has occurred while trying to get an existing entry
                Err(e) => return Err(e),
            };
            Ok((version, app_keys, permissions))
        })
        .and_then(move |(version, app_keys, permissions)| {
            access_container::put_entry(&c2, &app_id, &app_keys, &permissions, version)
        })
        .into_box()
}

/// Authenticate an app request.
///
/// First, this function searches for an app info in the access container.
/// If the app is found, then the `AuthGranted` struct is returned based on that information.
/// If the app is not found in the access container, then it will be authenticated.
pub fn authenticate(client: &AuthClient, auth_req: AuthReq) -> Box<AuthFuture<AuthGranted>> {
    let app_id = auth_req.app.id.clone();
    let permissions = auth_req.containers.clone();
    let AuthReq {
        app_container,
        app_permissions,
        ..
    } = auth_req;

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

    config::list_apps(client)
        .join(check_revocation(client, app_id.clone()))
        .and_then(move |((apps_version, apps), ())| {
            app_state(&c2, &apps, &app_id)
                .map(move |app_state| (apps_version, apps, app_state, app_id))
        })
        .and_then(move |(apps_version, mut apps, app_state, app_id)| {
            // Determine an app state. If it's revoked we can reuse existing
            // keys stored in the config. And if it is authorised, we just
            // return the app info from the config.
            match app_state {
                AppState::NotAuthenticated => {
                    let public_id = c3.public_id();
                    // Safe to unwrap as the auth client will have a client public id.
                    let keys = AppKeys::new(unwrap!(public_id.client_public_id()).clone());
                    let app = AppInfo {
                        info: auth_req.app,
                        keys,
                        perms: auth_req.app_permissions,
                    };
                    config::insert_app(&c3, apps, config::next_version(apps_version), app.clone())
                        .map(move |_| (app, app_state, app_id))
                        .into_box()
                }
                AppState::Authenticated | AppState::Revoked => {
                    let app_entry_name = sha3_256(app_id.as_bytes());
                    if let Some(app) = apps.remove(&app_entry_name) {
                        ok!((app, app_state, app_id))
                    } else {
                        err!(AuthError::from(
                            "Logical error - couldn't find a revoked app in config"
                        ))
                    }
                }
            }
        })
        .and_then(move |(app, app_state, app_id)| {
            match app_state {
                AppState::Authenticated => {
                    // Return info of the already registered app
                    authenticated_app(&c4, app, app_id, app_container, app_permissions)
                }
                AppState::NotAuthenticated | AppState::Revoked => {
                    // Register a new app or restore a previously registered app
                    authenticate_new_app(&c4, app, app_container, app_permissions, permissions)
                }
            }
        })
        .into_box()
}

/// Return info of an already registered app.
/// If `app_container` is `true` then we also create/update the dedicated container.
fn authenticated_app(
    client: &AuthClient,
    app: AppInfo,
    app_id: String,
    app_container: bool,
    _app_permissions: AppPermissions,
) -> Box<AuthFuture<AuthGranted>> {
    let c2 = client.clone();
    let c3 = client.clone();

    let app_keys = app.keys.clone();
    let app_pk = app.keys.public_key();
    let bootstrap_config = fry!(client::bootstrap_config());

    access_container::fetch_entry(client, &app_id, app_keys.clone())
        .and_then(move |(_version, perms)| {
            let perms = perms.unwrap_or_else(AccessContainerEntry::default);

            // TODO: check if we need to update app permissions

            // Check whether we need to create/update dedicated container
            if app_container && !app_container_exists(&perms, &app_id) {
                let future = app_container::fetch_or_create(&c2, &app_id, app_pk).and_then(
                    move |mdata_info| {
                        let perms = insert_app_container(perms, &app_id, mdata_info);
                        update_access_container(&c2, &app, perms.clone()).map(move |_| perms)
                    },
                );
                Either::A(future)
            } else {
                Either::B(future::ok(perms))
            }
        })
        .and_then(move |perms| {
            let access_container_info = c3.access_container();
            let access_container_info = AccessContInfo::from_mdata_info(&access_container_info)?;

            Ok(AuthGranted {
                app_keys,
                bootstrap_config,
                access_container_info,
                access_container_entry: perms,
            })
        })
        .into_box()
}

/// Register a new or revoked app in Maid Managers and in the access container.
///
/// 1. Insert app's key to Maid Managers
/// 2. Update container permissions for requested containers
/// 3. Create the app container (if it's been requested)
/// 4. Insert or update the access container entry for an app
/// 5. Return `AuthGranted`
fn authenticate_new_app(
    client: &AuthClient,
    app: AppInfo,
    app_container: bool,
    app_permissions: AppPermissions,
    permissions: HashMap<String, ContainerPermissions>,
) -> Box<AuthFuture<AuthGranted>> {
    let c2 = client.clone();
    let c3 = client.clone();
    let c4 = client.clone();
    let c5 = client.clone();
    let c6 = client.clone();

    let app_pk = app.keys.public_key();
    let app_keys = app.keys.clone();
    let app_keys_auth = app.keys.clone();
    let app_id = app.info.id.clone();

    client
        .list_auth_keys_and_version()
        .and_then(move |(_, version)| {
            recoverable_apis::ins_auth_key_to_client_h(
                &c2,
                app_keys.public_key(),
                app_permissions,
                version + 1,
            )
        })
        .map_err(AuthError::from)
        .and_then(move |_| {
            if permissions.is_empty() {
                ok!((Default::default(), app_pk))
            } else {
                update_container_perms(&c3, permissions, app_pk)
                    .map(move |perms| (perms, app_pk))
                    .into_box()
            }
        })
        .and_then(move |(perms, app_pk)| {
            if app_container {
                app_container::fetch_or_create(&c4, &app_id, app_pk)
                    .and_then(move |mdata_info| {
                        ok!(insert_app_container(perms, &app_id, mdata_info))
                    })
                    .map(move |perms| (perms, app))
                    .into_box()
            } else {
                ok!((perms, app))
            }
        })
        .and_then(move |(perms, app)| {
            update_access_container(&c5, &app, perms.clone()).map(move |_| perms)
        })
        .and_then(move |access_container_entry| {
            let access_container_info = c6.access_container();
            let access_container_info = AccessContInfo::from_mdata_info(&access_container_info)?;

            Ok(AuthGranted {
                app_keys: app_keys_auth,
                bootstrap_config: client::bootstrap_config()?,
                access_container_info,
                access_container_entry,
            })
        })
        .into_box()
}

fn check_revocation(client: &AuthClient, app_id: String) -> Box<AuthFuture<()>> {
    config::get_app_revocation_queue(client)
        .and_then(move |(_, queue)| {
            if queue.contains(&app_id) {
                Err(AuthError::PendingRevocation)
            } else {
                Ok(())
            }
        })
        .into_box()
}