Fossil

Documentation
Login

Documentation

/*
** Copyright (c) 2011 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
**
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*/
#include "VERSION.h"
#include "config.h"
#include "json_user.h"

#if INTERFACE
#include "json_detail.h"
#endif

static cson_value * json_user_get();
static cson_value * json_user_list();
static cson_value * json_user_save();
#if 0
static cson_value * json_user_create();

#endif

/*
** Mapping of /json/user/XXX commands/paths to callbacks.
*/
static const JsonPageDef JsonPageDefs_User[] = {
{"create", json_page_nyi, 0},
{"save", json_user_save, 0},
{"get", json_user_get, 0},
{"list", json_user_list, 0},
/* Last entry MUST have a NULL name. */
{NULL,NULL,0}
};


/*
** Implements the /json/user family of pages/commands.
**
*/
cson_value * json_page_user(){
  return json_page_dispatch_helper(&JsonPageDefs_User[0]);
}


/*
** Impl of /json/user/list. Requires admin rights.
*/
static cson_value * json_user_list(){
  cson_value * payV = NULL;
  Stmt q;
  if(!g.perm.Admin){
    g.json.resultCode = FSL_JSON_E_DENIED;
    return NULL;
  }
  db_prepare(&q,"SELECT uid AS uid,"
             " login AS name,"
             " cap AS capabilities,"
             " info AS info,"
             " mtime AS mtime"
             " FROM user ORDER BY login");
  payV = json_stmt_to_array_of_obj(&q, NULL);
  db_finalize(&q);
  if(NULL == payV){
    json_set_err(FSL_JSON_E_UNKNOWN,
                 "Could not convert user list to JSON.");
  }
  return payV;  
}

/*
** Impl of /json/user/get. Requires admin rights.
*/
static cson_value * json_user_get(){
  cson_value * payV = NULL;
  char const * pUser = NULL;
  Stmt q;
  if(!g.perm.Admin){
    json_set_err(FSL_JSON_E_DENIED,
                 "Requires 'a' privileges.");
    return NULL;
  }
  pUser = json_command_arg(g.json.dispatchDepth+1);
  if( g.isHTTP && (!pUser || !*pUser) ){
    pUser = json_getenv_cstr("name")
      /* ACHTUNG: fossil apparently internally sets name=user/get/XYZ
         if we pass the name as part of the path, which is why we check
         with json_command_path() before trying to get("name").
      */;
  }
  if(!pUser || !*pUser){
    json_set_err(FSL_JSON_E_MISSING_ARGS,"Missing 'name' property.");
    return NULL;
  }
  db_prepare(&q,"SELECT uid AS uid,"
             " login AS name,"
             " cap AS capabilities,"
             " info AS info,"
             " mtime AS mtime"
             " FROM user"
             " WHERE login=%Q",
             pUser);
  if( (SQLITE_ROW == db_step(&q)) ){
    payV = cson_sqlite3_row_to_object(q.pStmt);
    if(!payV){
      json_set_err(FSL_JSON_E_UNKNOWN,"Could not convert user row to JSON.");
    }
  }else{
    json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,"User not found.");
  }
  db_finalize(&q);
  return payV;  
}

/*
** Expects pUser to contain fossil user fields in JSON form: name,
** uid, info, capabilities, password.
**
** At least one of (name, uid) must be included. All others are
** optional and their db fields will not be updated if those fields
** are not included in pUser.
**
** If uid is specified then name may refer to a _new_ name
** for a user, otherwise the name must refer to an existing user.
**
** On error g.json's error state is set one of the FSL_JSON_E_xxx
** values from FossilJsonCodes is returned.
**
** On success the db record for the given user is updated.
**
** Requires either Admin, Setup, or Password access. Non-admin/setup
** users can only change their own information.
**
** TODOs:
**
** - Admin non-Setup users cannot change the information for Setup
** users.
**
*/
int json_user_update_from_json( cson_object const * pUser ){
#define CSTR(X) cson_string_cstr(cson_value_get_string( cson_object_get(pUser, X ) ))
  char const * zName = CSTR("name");
  char const * zNameOrig = zName;
  char * zNameFree = NULL;
  char const * zInfo = CSTR("info");
  char const * zCap = CSTR("capabilities");
  char const * zPW = CSTR("password");
  cson_value const * forceLogout = cson_object_get(pUser, "forceLogout");
  int gotFields = 0;
#undef CSTR
  cson_int_t uid = cson_value_get_integer( cson_object_get(pUser, "uid") );
  Blob sql = empty_blob;
  Stmt q = empty_Stmt;

  if(!g.perm.Admin && !g.perm.Setup && !g.perm.Password){
    return json_set_err( FSL_JSON_E_DENIED,
                         "Password change requires 'a', 's', "
                         "or 'p' permissions.");
  }
  
  if(uid<=0 && (!zName||!*zName)){
    return json_set_err(FSL_JSON_E_MISSING_ARGS,
                        "One of 'uid' or 'name' is required.");
  }else if(uid>0){
    zNameFree = db_text(NULL, "SELECT login FROM user WHERE uid=%d",uid);
    if(!zNameFree){
      return json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
                          "No login found for uid %d.", uid);
    }
    zName = zNameFree;
  }else{
    uid = db_int(0,"SELECT uid FROM user WHERE login=%Q",
                 zName);
    if(uid<=0){
      return json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
                          "No login found for user [%s].", zName);
    }
  }
  /*
    Todo: reserve the uid=-1 to mean that the user should be created
    by this request.
  */

  /* Maintenance note: all error-returns from here on out should go
     via goto error in order to clean up.
  */
  
  if(uid != g.userUid){
    /*
      TODO: do not allow an admin user to modify a setup user
      unless the admin is also a setup user. setup.c uses
      that logic.
    */
    if(!g.perm.Admin && !g.perm.Setup){
      json_set_err(FSL_JSON_E_DENIED,
                   "Changing another user's data requires "
                   "'a' or 's' privileges.");
    }
  }
  
  blob_append(&sql, "UPDATE USER SET",-1 );
  blob_append(&sql, " mtime=cast(strftime('%s') AS INTEGER)", -1);

  if((uid>0) && zName
     && zNameOrig && (zName != zNameOrig)
     && (0!=strcmp(zNameOrig,zName))){
    /* Only change the name if the uid is explicitly set and name
       would actually change. */
    if(!g.perm.Admin && !g.perm.Setup) {
      json_set_err( FSL_JSON_E_DENIED,
                    "Modifying user names requires 'a' or 's' privileges.");
      goto error;
    }
    blob_appendf(&sql, ", login=%Q", zNameOrig);
    ++gotFields;
  }

  if( zCap ){
    blob_appendf(&sql, ", cap=%Q", zCap);
    ++gotFields;
  }

  if( zPW ){
    char * zPWHash = NULL;
    ++gotFields;
    zPWHash = sha1_shared_secret(zPW, zName, NULL);
    blob_appendf(&sql, ", pw=%Q", zPWHash);
    free(zPWHash);
  }

  if( zInfo ){
    blob_appendf(&sql, ", info=%Q", zInfo);
    ++gotFields;
  }

  if((g.perm.Admin || g.perm.Setup)
     && forceLogout && cson_value_get_bool(forceLogout)){
    blob_append(&sql, ", cookie=NULL, cexpire=NULL", -1);
    ++gotFields;
  }
  
  if(!gotFields){
    json_set_err( FSL_JSON_E_MISSING_ARGS,
                  "Required user data are missing.");
    goto error;
  }
  assert(uid>0);
  blob_appendf(&sql, " WHERE uid=%d", uid);
  free( zNameFree );
  /*puts(blob_str(&sql));*/
  db_prepare(&q, "%s", blob_str(&sql));
  blob_reset(&sql);
  db_exec(&q);
  db_finalize(&q);
  return 0;

  error:
  assert(0 != g.json.resultCode);
  free(zNameFree);
  blob_reset(&sql);
  return g.json.resultCode;
}


/*
** Impl of /json/user/save.
**
** TODOs:
**
** - Return something useful in the payload (at least the id of the
** modified/created user).
*/
static cson_value * json_user_save(){
  if(! g.json.reqPayload.o ){
    json_set_err(FSL_JSON_E_MISSING_ARGS,
                 "User data must be contained in the request payload.");
    return NULL;

  }
  json_user_update_from_json( g.json.reqPayload.o );
  return NULL;
}