Merge pull request #110 from sismics/master

Push to production
This commit is contained in:
Benjamin Gamard 2016-06-16 21:38:14 +02:00 committed by GitHub
commit 05bfaa0035
48 changed files with 1341 additions and 512 deletions

View File

@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.android.tools.build:gradle:2.1.2'
}
}
apply plugin: 'com.android.application'
@ -14,7 +14,7 @@ repositories {
android {
compileSdkVersion 23
buildToolsVersion '23.0.3'
buildToolsVersion '24'
defaultConfig {
minSdkVersion 14
@ -50,13 +50,13 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'
compile 'com.android.support:design:23.3.0'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:recyclerview-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
compile 'org.greenrobot:eventbus:3.0.0'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.1.1'
compile "com.squareup.okhttp3:okhttp-urlconnection:3.1.1"
compile 'com.squareup.okhttp3:okhttp:3.3.1'
compile "com.squareup.okhttp3:okhttp-urlconnection:3.3.1"
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
}

View File

@ -101,7 +101,7 @@ public class DocumentEditActivity extends AppCompatActivity {
finish();
return;
}
JSONArray tagArray = tags.optJSONArray("stats");
JSONArray tagArray = tags.optJSONArray("tags");
List<JSONObject> tagList = new ArrayList<>();
for (int i = 0; i < tagArray.length(); i++) {

View File

@ -16,6 +16,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.method.LinkMovementMethod;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
@ -51,7 +52,7 @@ import com.sismics.docs.resource.FileResource;
import com.sismics.docs.service.FileUploadService;
import com.sismics.docs.util.NetworkUtil;
import com.sismics.docs.util.PreferenceUtil;
import com.sismics.docs.util.TagUtil;
import com.sismics.docs.util.SpannableUtil;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@ -176,9 +177,11 @@ public class DocumentViewActivity extends AppCompatActivity {
}
// Fill the layout
// Create date
TextView createdDateTextView = (TextView) findViewById(R.id.createdDateTextView);
createdDateTextView.setText(date);
// Description
TextView descriptionTextView = (TextView) findViewById(R.id.descriptionTextView);
if (description.isEmpty() || document.isNull("description")) {
descriptionTextView.setVisibility(View.GONE);
@ -187,17 +190,20 @@ public class DocumentViewActivity extends AppCompatActivity {
descriptionTextView.setText(description);
}
// Tags
TextView tagTextView = (TextView) findViewById(R.id.tagTextView);
if (tags.length() == 0) {
tagTextView.setVisibility(View.GONE);
} else {
tagTextView.setVisibility(View.VISIBLE);
tagTextView.setText(TagUtil.buildSpannable(tags));
tagTextView.setText(SpannableUtil.buildSpannableTags(tags));
}
// Language
ImageView languageImageView = (ImageView) findViewById(R.id.languageImageView);
languageImageView.setImageResource(getResources().getIdentifier(language, "drawable", getPackageName()));
// Shared status
ImageView sharedImageView = (ImageView) findViewById(R.id.sharedImageView);
sharedImageView.setVisibility(shared ? View.VISIBLE : View.GONE);
@ -642,10 +648,10 @@ public class DocumentViewActivity extends AppCompatActivity {
}
// Action only available if the document is writable
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.GONE);
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.GONE);
// ACLs
ListView aclListView = (ListView) findViewById(R.id.aclListView);
@ -679,10 +685,54 @@ public class DocumentViewActivity extends AppCompatActivity {
startActivity(intent);
}
});
// Contributors
TextView contributorsTextView = (TextView) findViewById(R.id.contributorsTextView);
contributorsTextView.setText(SpannableUtil.buildSpannableContributors(document.optJSONArray("contributors")));
// Relations
JSONArray relations = document.optJSONArray("relations");
if (relations.length() > 0) {
TextView relationsTextView = (TextView) findViewById(R.id.relationsTextView);
relationsTextView.setMovementMethod(LinkMovementMethod.getInstance());
relationsTextView.setText(SpannableUtil.buildSpannableRelations(relations));
} else {
findViewById(R.id.relationsLayout).setVisibility(View.GONE);
}
// Additional dublincore metadata
displayDublincoreMetadata(R.id.subjectTextView, R.id.subjectLayout, "subject");
displayDublincoreMetadata(R.id.identifierTextView, R.id.identifierLayout, "identifier");
displayDublincoreMetadata(R.id.publisherTextView, R.id.publisherLayout, "publisher");
displayDublincoreMetadata(R.id.formatTextView, R.id.formatLayout, "format");
displayDublincoreMetadata(R.id.sourceTextView, R.id.sourceLayout, "source");
displayDublincoreMetadata(R.id.typeTextView, R.id.typeLayout, "type");
displayDublincoreMetadata(R.id.coverageTextView, R.id.coverageLayout, "coverage");
displayDublincoreMetadata(R.id.rightsTextView, R.id.rightsLayout, "rights");
}
});
}
/**
* Display a dublincore metadata.
*
* @param textViewId TextView ID
* @param blockViewId View ID
* @param name Name
*/
private void displayDublincoreMetadata(int textViewId, int blockViewId, String name) {
if (document == null) return;
String value = document.optString(name);
if (document.isNull(name) || value.isEmpty()) {
findViewById(blockViewId).setVisibility(View.GONE);
return;
}
findViewById(blockViewId).setVisibility(View.VISIBLE);
TextView textView = (TextView) findViewById(textViewId);
textView.setText(value);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
switch (view.getId()) {

View File

@ -9,7 +9,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.sismics.docs.R;
import com.sismics.docs.util.TagUtil;
import com.sismics.docs.util.SpannableUtil;
import org.json.JSONArray;
import org.json.JSONObject;
@ -69,7 +69,7 @@ public class DocListAdapter extends RecyclerView.Adapter<DocListAdapter.ViewHold
holder.titleTextView.setText(document.optString("title"));
JSONArray tags = document.optJSONArray("tags");
holder.subtitleTextView.setText(TagUtil.buildSpannable(tags));
holder.subtitleTextView.setText(SpannableUtil.buildSpannableTags(tags));
String date = DateFormat.getDateFormat(holder.dateTextView.getContext()).format(new Date(document.optLong("create_date")));
holder.dateTextView.setText(date);

View File

@ -73,7 +73,7 @@ public class SearchFragment extends DialogFragment {
dialog.cancel();
return dialog;
}
JSONArray tagArray = tags.optJSONArray("stats");
JSONArray tagArray = tags.optJSONArray("tags");
List<JSONObject> tagList = new ArrayList<>();
for (int i = 0; i < tagArray.length(); i++) {

View File

@ -0,0 +1,33 @@
package com.sismics.docs.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ListView;
/**
* Non-scrollable ListView.
* All items are visible from the start.
*
* @author http://stackoverflow.com/questions/18813296/non-scrollable-listview-inside-scrollview/24629341#24629341
*/
public class NonScrollListView extends ListView {
public NonScrollListView(Context context) {
super(context);
}
public NonScrollListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NonScrollListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
ViewGroup.LayoutParams params = getLayoutParams();
params.height = getMeasuredHeight();
}
}

View File

@ -0,0 +1,85 @@
package com.sismics.docs.util;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Utility class for spannable.
*
* @author bgamard.
*/
public class SpannableUtil {
/**
* Create a colored spannable from tags.
*
* @param tags Tags
* @return Colored spannable
*/
public static Spannable buildSpannableTags(JSONArray tags) {
return buildSpannable(tags, "name", "color");
}
/**
* Create a spannable for contributors.
*
* @param contributors Contributors
* @return Spannable
*/
public static Spannable buildSpannableContributors(JSONArray contributors) {
return buildSpannable(contributors, "username", null);
}
/**
* Create a spannable for relations.
*
* @param relations Relations
* @return Spannable
*/
public static Spannable buildSpannableRelations(JSONArray relations) {
return buildSpannable(relations, "title", null);
}
/**
* Create a spannable from a JSONArray.
*
* @param array JSONArray
* @param valueName Name of the value part
* @param colorName Name of the color part (optional)
* @return Spannable
*/
private static Spannable buildSpannable(JSONArray array, String valueName, String colorName) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < array.length(); i++) {
final JSONObject tag = array.optJSONObject(i);
int start = builder.length();
builder.append(" ").append(tag.optString(valueName)).append(" ");
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString(colorName, "#5bc0de"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
/*
TODO : Make tags, relations and contributors clickable
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.WHITE);
ds.setUnderlineText(false);
}
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);*/
builder.append(" ");
}
return builder;
}
}

View File

@ -1,39 +0,0 @@
package com.sismics.docs.util;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Utility class for tags.
*
* @author bgamard.
*/
public class TagUtil {
/**
* Create a colored spannable from tags.
*
* @param tags Tags
* @return Colored spannable
*/
public static Spannable buildSpannable(JSONArray tags) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < tags.length(); i++) {
JSONObject tag = tags.optJSONObject(i);
int start = builder.length();
builder.append(" ").append(tag.optString("name")).append(" ");
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString("color"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.append(" ");
}
return builder;
}
}

View File

@ -142,241 +142,523 @@
<!-- Right drawer -->
<LinearLayout
<ScrollView
android:id="@+id/right_drawer"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:orientation="vertical"
android:clickable="true"
android:background="#fff"
android:elevation="5dp">
<!-- Actions -->
android:elevation="5dp"
android:layout_gravity="end">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:orientation="vertical">
<!-- Actions -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
style="?android:buttonBarStyle">
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/actionEditDocument"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_create_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/edit_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
android:orientation="horizontal"
style="?android:buttonBarStyle">
<Button
android:id="@+id/actionUploadFile"
<Button
android:id="@+id/actionDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_download_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/download_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
<Button
android:id="@+id/actionExportPdf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_description_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/export_pdf"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
<Button
android:id="@+id/actionAuditLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_assignment_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/activity"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_upload_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/upload_file"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
android:orientation="horizontal"
style="?android:buttonBarStyle">
<Button
android:id="@+id/actionDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_download_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/download_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="8dp"/>
<Button
android:id="@+id/actionEditDocument"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_create_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/edit_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<Button
android:id="@+id/actionUploadFile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_file_upload_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/upload_file"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<Button
android:id="@+id/actionSharing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_share_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/share"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<Button
android:id="@+id/actionDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_delete_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/delete_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- Document metadata -->
<RelativeLayout
android:id="@+id/detailLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
style="?android:buttonBarStyle">
android:padding="12dp">
<Button
android:id="@+id/actionExportPdf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_description_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/export_pdf"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<TextView
android:id="@+id/createdDateLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/created_date"/>
<Button
android:id="@+id/actionSharing"
<TextView
android:id="@+id/createdDateTextView"
android:layout_toRightOf="@id/createdDateLabel"
android:layout_toEndOf="@id/createdDateLabel"
android:layout_toLeftOf="@id/sharedImageView"
android:layout_toStartOf="@id/sharedImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_share_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/share"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:fontFamily="sans-serif-light"/>
<Button
android:id="@+id/actionAuditLog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_assignment_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/activity"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
<TextView
android:id="@+id/creatorLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/creator"/>
<Button
android:id="@+id/actionDelete"
<TextView
android:id="@+id/creatorTextView"
android:layout_toRightOf="@id/creatorLabel"
android:layout_toEndOf="@id/creatorLabel"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateTextView"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/tagTextView"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_delete_grey600_24dp"
style="?android:buttonBarButtonStyle"
android:text="@string/delete_document"
android:textColor="#ff5a595b"
android:textAllCaps="false"
android:layout_margin="0dp"/>
android:layout_below="@id/creatorLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/descriptionTextView"
android:layout_marginTop="12dp"
android:layout_below="@id/tagTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
<ImageView
android:contentDescription="@string/shared"
android:id="@+id/sharedImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_folder_shared_grey600_24dp"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_toLeftOf="@+id/languageImageView"
android:layout_toStartOf="@+id/languageImageView"/>
<ImageView
android:contentDescription="@string/language"
android:id="@+id/languageImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>
<!-- Additional dublincore metadata -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingBottom="12dp">
<!-- Subject -->
<LinearLayout
android:id="@+id/subjectLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/subject"/>
<TextView
android:id="@+id/subjectTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Identifier -->
<LinearLayout
android:id="@+id/identifierLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/identifier"/>
<TextView
android:id="@+id/identifierTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Publisher -->
<LinearLayout
android:id="@+id/publisherLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/publisher"/>
<TextView
android:id="@+id/publisherTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Format -->
<LinearLayout
android:id="@+id/formatLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/format"/>
<TextView
android:id="@+id/formatTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Source -->
<LinearLayout
android:id="@+id/sourceLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/source"/>
<TextView
android:id="@+id/sourceTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Type -->
<LinearLayout
android:id="@+id/typeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/type"/>
<TextView
android:id="@+id/typeTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Coverage -->
<LinearLayout
android:id="@+id/coverageLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/coverage"/>
<TextView
android:id="@+id/coverageTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Rights -->
<LinearLayout
android:id="@+id/rightsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/rights"/>
<TextView
android:id="@+id/rightsTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Contributors -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/contributors"/>
<TextView
android:id="@+id/contributorsTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
<!-- Relations -->
<LinearLayout
android:id="@+id/relationsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="2dp">
<TextView
android:layout_weight="0.33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/relations"/>
<TextView
android:id="@+id/relationsTextView"
android:layout_weight="0.67"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- ACLs -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#de000000"
android:text="@string/who_can_access"
android:layout_margin="12dp"/>
<com.sismics.docs.ui.view.NonScrollListView
android:id="@+id/aclListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- Document metadata -->
<RelativeLayout
android:id="@+id/detailLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:id="@+id/createdDateLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/created_date"/>
<TextView
android:id="@+id/createdDateTextView"
android:layout_toRightOf="@id/createdDateLabel"
android:layout_toEndOf="@id/createdDateLabel"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/creatorLabel"
android:layout_width="100dp"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif"
android:text="@string/creator"/>
<TextView
android:id="@+id/creatorTextView"
android:layout_toRightOf="@id/creatorLabel"
android:layout_toEndOf="@id/creatorLabel"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:layout_below="@+id/createdDateTextView"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/tagTextView"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/creatorLabel"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:maxLines="1"
android:fontFamily="sans-serif-light"/>
<TextView
android:id="@+id/descriptionTextView"
android:layout_marginTop="12dp"
android:layout_below="@id/tagTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"/>
<ImageView
android:id="@+id/sharedImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_folder_shared_grey600_24dp"
android:layout_alignParentTop="true"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_toLeftOf="@+id/languageImageView"
android:layout_toStartOf="@+id/languageImageView"/>
<ImageView
android:id="@+id/languageImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:background="#eee"/>
<!-- ACLs -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#de000000"
android:text="@string/who_can_access"
android:layout_margin="12dp"/>
<ListView
android:id="@+id/aclListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"/>
</LinearLayout>
</ScrollView>
</android.support.v4.widget.DrawerLayout>

View File

@ -132,5 +132,17 @@
<string name="storage_quota">Storage quota</string>
<string name="storage_display">%1$d/%2$d MB</string>
<string name="validation_code">Validation code</string>
<string name="shared">Shared</string>
<string name="language">Language</string>
<string name="coverage">Coverage</string>
<string name="type">Type</string>
<string name="source">Source</string>
<string name="format">Format</string>
<string name="publisher">Publisher</string>
<string name="identifier">Identifier</string>
<string name="subject">Subject</string>
<string name="rights">Rights</string>
<string name="contributors">Contributors</string>
<string name="relations">Relations</string>
</resources>

View File

@ -12,6 +12,8 @@
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx3072m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View File

@ -13,5 +13,10 @@ public enum ConfigType {
/**
* Theme configuration.
*/
THEME
THEME,
/**
* Guest login.
*/
GUEST_LOGIN
}

View File

@ -29,7 +29,12 @@ public class Constants {
* File Lucene directory storage.
*/
public static final String LUCENE_DIRECTORY_STORAGE_FILE = "FILE";
/**
* Guest user ID.
*/
public static final String GUEST_USER_ID = "guest";
/**
* Default generic user role.
*/

View File

@ -1 +1 @@
db.version=9
db.version=10

View File

@ -0,0 +1,3 @@
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('GUEST_LOGIN', 'false');
insert into T_USER(USE_ID_C, USE_IDROLE_C, USE_USERNAME_C, USE_PASSWORD_C, USE_EMAIL_C, USE_CREATEDATE_D, USE_PRIVATEKEY_C) values('guest', 'user', 'guest', '', 'guest@localhost', NOW(), 'GuestPk');
update T_CONFIG set CFG_VALUE_C = '10' where CFG_ID_C = 'DB_VERSION';

View File

@ -59,4 +59,9 @@ public class AnonymousPrincipal implements IPrincipal {
public Set<String> getGroupIdSet() {
return Sets.newHashSet();
}
@Override
public boolean isGuest() {
return false;
}
}

View File

@ -18,6 +18,13 @@ public interface IPrincipal extends Principal {
*/
boolean isAnonymous();
/**
* Checks if the principal is a guest.
*
* @return True if the principal is a guest
*/
boolean isGuest();
/**
* Returns the ID of the connected user, or null if the user is anonymous
*

View File

@ -2,6 +2,7 @@ package com.sismics.security;
import java.util.Set;
import com.sismics.docs.core.constant.Constants;
import org.joda.time.DateTimeZone;
/**
@ -108,4 +109,9 @@ public class UserPrincipal implements IPrincipal {
public void setGroupIdSet(Set<String> groupIdSet) {
this.groupIdSet = groupIdSet;
}
@Override
public boolean isGuest() {
return Constants.GUEST_USER_ID.equals(id);
}
}

View File

@ -0,0 +1,47 @@
package com.sismics.util.filter;
import com.google.common.base.Strings;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.model.jpa.User;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
/**
* A header-based security filter that authenticates an user using the "X-Authenticated-User" request header as the user ID.
* This filter is intended to be used in conjunction with an external authenticating proxy.
*
* @author pacien
*/
public class HeaderBasedSecurityFilter extends SecurityFilter {
/**
* Authentication header.
*/
public static final String AUTHENTICATED_USER_HEADER = "X-Authenticated-User";
/**
* True if this authentication method is enabled.
*/
private boolean enabled;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
enabled = Boolean.parseBoolean(filterConfig.getInitParameter("enabled"))
|| Boolean.parseBoolean(System.getProperty("docs.header_authentication"));
}
@Override
protected User authenticate(HttpServletRequest request) {
if (!enabled) {
return null;
}
String username = request.getHeader(AUTHENTICATED_USER_HEADER);
if (Strings.isNullOrEmpty(username)) {
return null;
}
return new UserDao().getActiveByUsername(username);
}
}

View File

@ -0,0 +1,145 @@
package com.sismics.util.filter;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.GroupDao;
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
import com.sismics.docs.core.dao.jpa.dto.GroupDto;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.security.AnonymousPrincipal;
import com.sismics.security.UserPrincipal;
import jersey.repackaged.com.google.common.collect.Sets;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An abstract security filter for user authentication, that injects corresponding users into the request.
* Successfully authenticated users are injected as UserPrincipal, or as AnonymousPrincipal otherwise.
* If an user has already been authenticated for the request, no further authentication attempt is made.
*
* @author pacien
* @author jtremeaux
*/
public abstract class SecurityFilter implements Filter {
/**
* Name of the attribute containing the principal.
*/
public static final String PRINCIPAL_ATTRIBUTE = "principal";
/**
* Logger.
*/
static final Logger LOG = LoggerFactory.getLogger(SecurityFilter.class);
/**
* Returns true if the supplied request has an UserPrincipal.
*
* @param request HTTP request
* @return True if the supplied request has an UserPrincipal
*/
private boolean hasIdentifiedUser(HttpServletRequest request) {
return request.getAttribute(PRINCIPAL_ATTRIBUTE) instanceof UserPrincipal;
}
/**
* Injects the given user into the request, with the appropriate authentication state.
*
* @param request HTTP request
* @param user nullable User to inject
*/
private void injectUser(HttpServletRequest request, User user) {
// Check if the user is still valid
if (user != null && user.getDeleteDate() == null) {
injectAuthenticatedUser(request, user);
} else {
injectAnonymousUser(request);
}
}
/**
* Inject an authenticated user into the request attributes.
*
* @param request HTTP request
* @param user User to inject
*/
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
// Add groups
GroupDao groupDao = new GroupDao();
Set<String> groupRoleIdSet = new HashSet<>();
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria()
.setUserId(user.getId())
.setRecursive(true), null);
Set<String> groupIdSet = Sets.newHashSet();
for (GroupDto groupDto : groupDtoList) {
groupIdSet.add(groupDto.getId());
if (groupDto.getRoleId() != null) {
groupRoleIdSet.add(groupDto.getRoleId());
}
}
userPrincipal.setGroupIdSet(groupIdSet);
// Add base functions
groupRoleIdSet.add(user.getRoleId());
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet);
userPrincipal.setBaseFunctionSet(baseFunctionSet);
// Add email
userPrincipal.setEmail(user.getEmail());
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
}
/**
* Inject an anonymous user into the request attributes.
*
* @param request HTTP request
*/
private void injectAnonymousUser(HttpServletRequest request) {
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// NOP
}
@Override
public void destroy() {
// NOP
}
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (!hasIdentifiedUser(request)) {
User user = authenticate(request);
injectUser(request, user);
}
filterChain.doFilter(request, response);
}
/**
* Authenticates an user from the given request parameters.
*
* @param request HTTP request
* @return nullable User
*/
protected abstract User authenticate(HttpServletRequest request);
}

View File

@ -1,38 +1,14 @@
package com.sismics.util.filter;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao;
import com.sismics.docs.core.dao.jpa.GroupDao;
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
import com.sismics.docs.core.dao.jpa.dto.GroupDto;
import com.sismics.docs.core.model.jpa.AuthenticationToken;
import com.sismics.docs.core.model.jpa.User;
import com.sismics.security.AnonymousPrincipal;
import com.sismics.security.UserPrincipal;
import jersey.repackaged.com.google.common.collect.Sets;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.text.MessageFormat;
import java.util.Date;
/**
* This filter is used to authenticate the user having an active session via an authentication token stored in database.
@ -42,22 +18,12 @@ import jersey.repackaged.com.google.common.collect.Sets;
*
* @author jtremeaux
*/
public class TokenBasedSecurityFilter implements Filter {
/**
* Logger.
*/
private static final Logger log = LoggerFactory.getLogger(TokenBasedSecurityFilter.class);
public class TokenBasedSecurityFilter extends SecurityFilter {
/**
* Name of the cookie used to store the authentication token.
*/
public static final String COOKIE_NAME = "auth_token";
/**
* Name of the attribute containing the principal.
*/
public static final String PRINCIPAL_ATTRIBUTE = "principal";
/**
* Lifetime of the authentication token in seconds, since login.
*/
@ -66,68 +32,40 @@ public class TokenBasedSecurityFilter implements Filter {
/**
* Lifetime of the authentication token in seconds, since last connection.
*/
public static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// NOP
}
private static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
@Override
public void destroy() {
// NOP
}
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// Get the value of the client authentication token
HttpServletRequest request = (HttpServletRequest) req;
String authToken = null;
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (COOKIE_NAME.equals(cookie.getName())) {
authToken = cookie.getValue();
/**
* Extracts and returns an authentication token from a cookie list.
*
* @param cookies Cookie list
* @return nullable auth token
*/
private String extractAuthToken(Cookie[] cookies) {
if (cookies != null) {
for (Cookie cookie : cookies) {
if (COOKIE_NAME.equals(cookie.getName()) && !cookie.getValue().isEmpty()) {
return cookie.getValue();
}
}
}
// Get the corresponding server token
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
AuthenticationToken authenticationToken = null;
if (authToken != null) {
authenticationToken = authenticationTokenDao.get(authToken);
}
if (authenticationToken == null) {
injectAnonymousUser(request);
} else {
// Check if the token is still valid
if (isTokenExpired(authenticationToken)) {
try {
injectAnonymousUser(request);
// Destroy the expired token
authenticationTokenDao.delete(authToken);
} catch (Exception e) {
if (log.isErrorEnabled()) {
log.error(MessageFormat.format("Error deleting authentication token {0} ", authToken), e);
}
}
} else {
// Check if the user is still valid
UserDao userDao = new UserDao();
User user = userDao.getById(authenticationToken.getUserId());
if (user != null && user.getDeleteDate() == null) {
injectAuthenticatedUser(request, user);
} else {
injectAnonymousUser(request);
}
}
}
filterChain.doFilter(request, response);
return null;
}
/**
* Deletes an expired authentication token.
*
* @param authTokenID auth token ID
*/
private void handleExpiredToken(AuthenticationTokenDao dao, String authTokenID) {
try {
dao.delete(authTokenID);
} catch (Exception e) {
if (LOG.isErrorEnabled())
LOG.error(MessageFormat.format("Error deleting authentication token {0} ", authTokenID), e);
}
}
/**
* Returns true if the token is expired.
*
@ -146,51 +84,27 @@ public class TokenBasedSecurityFilter implements Filter {
}
}
/**
* Inject an authenticated user into the request attributes.
*
* @param request HTTP request
* @param user User to inject
*/
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
// Add groups
GroupDao groupDao = new GroupDao();
Set<String> groupRoleIdSet = new HashSet<>();
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria()
.setUserId(user.getId())
.setRecursive(true), null);
Set<String> groupIdSet = Sets.newHashSet();
for (GroupDto groupDto : groupDtoList) {
groupIdSet.add(groupDto.getId());
if (groupDto.getRoleId() != null) {
groupRoleIdSet.add(groupDto.getRoleId());
}
@Override
protected User authenticate(HttpServletRequest request) {
// Get the value of the client authentication token
String authTokenId = extractAuthToken(request.getCookies());
if (authTokenId == null) {
return null;
}
userPrincipal.setGroupIdSet(groupIdSet);
// Add base functions
groupRoleIdSet.add(user.getRoleId());
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet);
userPrincipal.setBaseFunctionSet(baseFunctionSet);
// Add email
userPrincipal.setEmail(user.getEmail());
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
}
/**
* Inject an anonymous user into the request attributes.
*
* @param request HTTP request
*/
private void injectAnonymousUser(HttpServletRequest request) {
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
// Get the corresponding server token
AuthenticationTokenDao authTokenDao = new AuthenticationTokenDao();
AuthenticationToken authToken = authTokenDao.get(authTokenId);
if (authToken == null) {
return null;
}
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal);
if (isTokenExpired(authToken)) {
handleExpiredToken(authTokenDao, authTokenId);
return null;
}
authTokenDao.updateLastConnectionDate(authToken.getId());
return new UserDao().getById(authToken.getUserId());
}
}

View File

@ -5,6 +5,7 @@ import java.net.URI;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.UriBuilder;
import com.sismics.util.filter.HeaderBasedSecurityFilter;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.servlet.ServletRegistration;
import org.glassfish.grizzly.servlet.WebappContext;
@ -62,7 +63,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
@Before
public void setUp() throws Exception {
super.setUp();
System.setProperty("docs.header_authentication", "true");
clientUtil = new ClientUtil(target());
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
@ -71,6 +73,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
.addMappingForUrlPatterns(null, "/*");
context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class)
.addMappingForUrlPatterns(null, "/*");
context.addFilter("headerBasedSecurityFilter", HeaderBasedSecurityFilter.class)
.addMappingForUrlPatterns(null, "/*");
ServletRegistration reg = context.addServlet("jerseyServlet", ServletContainer.class);
reg.setInitParameter("jersey.config.server.provider.packages", "com.sismics.docs.rest.resource");
reg.setInitParameter("jersey.config.server.provider.classnames", "org.glassfish.jersey.media.multipart.MultiPartFeature");

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=9
db.version=10

View File

@ -14,15 +14,12 @@ import javax.json.JsonArrayBuilder;
import javax.json.JsonObjectBuilder;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.AclDao;
import com.sismics.docs.core.dao.jpa.TagDao;
import com.sismics.docs.core.dao.jpa.*;
import com.sismics.docs.core.dao.jpa.criteria.TagCriteria;
import com.sismics.docs.core.dao.jpa.dto.AclDto;
import com.sismics.docs.core.dao.jpa.dto.TagDto;
@ -33,8 +30,6 @@ import org.apache.log4j.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sismics.docs.core.dao.jpa.FileDao;
import com.sismics.docs.core.dao.jpa.UserDao;
import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.User;
@ -70,32 +65,58 @@ public class AppResource extends BaseResource {
* @apiGroup App
* @apiSuccess {String} current_version API current version
* @apiSuccess {String} min_version API minimum version
* @apiSuccess {Boolean} guest_login True if guest login is enabled
* @apiSuccess {String} total_memory Allocated JVM memory (in bytes)
* @apiSuccess {String} free_memory Free JVM memory (in bytes)
* @apiError (client) ForbiddenError Access denied
* @apiPermission user
* @apiPermission none
* @apiVersion 1.5.0
*
* @return Response
*/
@GET
public Response info() {
if (!authenticate()) {
throw new ForbiddenClientException();
}
ResourceBundle configBundle = ConfigUtil.getConfigBundle();
String currentVersion = configBundle.getString("api.current_version");
String minVersion = configBundle.getString("api.min_version");
Boolean guestLogin = ConfigUtil.getConfigBooleanValue(ConfigType.GUEST_LOGIN);
JsonObjectBuilder response = Json.createObjectBuilder()
.add("current_version", currentVersion.replace("-SNAPSHOT", ""))
.add("min_version", minVersion)
.add("guest_login", guestLogin)
.add("total_memory", Runtime.getRuntime().totalMemory())
.add("free_memory", Runtime.getRuntime().freeMemory());
return Response.ok().entity(response.build()).build();
}
/**
* Enable/disable guest login.
*
* @api {post} /app/guest_login Enable/disable guest login
* @apiName PostAppGuestLogin
* @apiGroup App
* @apiParam {Boolean} enabled If true, enable guest login
* @apiError (client) ForbiddenError Access denied
* @apiPermission admin
* @apiVersion 1.5.0
*
* @param enabled If true, enable guest login
* @return Response
*/
@POST
@Path("guest_login")
public Response guestLogin(@FormParam("enabled") Boolean enabled) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
ConfigDao configDao = new ConfigDao();
configDao.update(ConfigType.GUEST_LOGIN, enabled.toString());
return Response.ok().build();
}
/**
* Retrieve the application logs.
@ -325,7 +346,7 @@ public class AppResource extends BaseResource {
/**
* Recompute the quota for each user.
*
* @api {post} /app/batch/recompute_quote Recompute user quotas
* @api {post} /app/batch/recompute_quota Recompute user quotas
* @apiName PostAppBatchRecomputeQuota
* @apiGroup App
* @apiSuccess {String} status Status OK
@ -385,7 +406,7 @@ public class AppResource extends BaseResource {
/**
* Add base ACLs to tags.
*
* @api {post} /app/batch/recompute_quote Add base ACL to tags
* @api {post} /app/batch/tag_acls Add base ACL to tags
* @apiDescription This resource must be used after migrating to 1.5.
* It will not do anything if base ACL are already present on tags.
* @apiName PostAppBatchTagAcls

View File

@ -1,19 +1,18 @@
package com.sismics.docs.rest.resource;
import java.security.Principal;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import com.google.common.collect.Lists;
import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.security.IPrincipal;
import com.sismics.security.UserPrincipal;
import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sismics.util.filter.SecurityFilter;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import java.security.Principal;
import java.util.List;
import java.util.Set;
/**
* Base class of REST resources.
@ -67,7 +66,7 @@ public abstract class BaseResource {
* @return True if the user is authenticated and not anonymous
*/
protected boolean authenticate() {
Principal principal = (Principal) request.getAttribute(TokenBasedSecurityFilter.PRINCIPAL_ATTRIBUTE);
Principal principal = (Principal) request.getAttribute(SecurityFilter.PRINCIPAL_ATTRIBUTE);
if (principal != null && principal instanceof IPrincipal) {
this.principal = (IPrincipal) principal;
return !this.principal.isAnonymous();

View File

@ -22,6 +22,8 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.util.ConfigUtil;
import org.apache.commons.lang.StringUtils;
import com.google.common.base.Strings;
@ -150,7 +152,7 @@ public class UserResource extends BaseResource {
* @apiParam {String{8..50}} password Password
* @apiParam {String{1..100}} email E-mail
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ForbiddenError Access denied or connected as guest
* @apiError (client) ValidationError Validation error
* @apiPermission user
* @apiVersion 1.5.0
@ -163,7 +165,7 @@ public class UserResource extends BaseResource {
public Response update(
@FormParam("password") String password,
@FormParam("email") String email) {
if (!authenticate()) {
if (!authenticate() || principal.isGuest()) {
throw new ForbiddenClientException();
}
@ -301,7 +303,7 @@ public class UserResource extends BaseResource {
* @apiName PostUserLogin
* @apiGroup User
* @apiParam {String} username Username
* @apiParam {String} password Password
* @apiParam {String} password Password (optional for guest login)
* @apiParam {String} code TOTP validation code
* @apiParam {Boolean} remember If true, create a long lasted token
* @apiSuccess {String} auth_token A cookie named auth_token containing the token ID
@ -328,7 +330,16 @@ public class UserResource extends BaseResource {
// Get the user
UserDao userDao = new UserDao();
User user = userDao.authenticate(username, password);
User user = null;
if (Constants.GUEST_USER_ID.equals(username)) {
if (ConfigUtil.getConfigBooleanValue(ConfigType.GUEST_LOGIN)) {
// Login as guest
user = userDao.getActiveByUsername(Constants.GUEST_USER_ID);
}
} else {
// Login as a normal user
user = userDao.authenticate(username, password);
}
if (user == null) {
throw new ForbiddenClientException();
}
@ -429,7 +440,7 @@ public class UserResource extends BaseResource {
* @apiName DeleteUser
* @apiGroup User
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied or the admin user cannot be deleted
* @apiError (client) ForbiddenError Access denied or the user cannot be deleted
* @apiPermission user
* @apiVersion 1.5.0
*
@ -442,8 +453,8 @@ public class UserResource extends BaseResource {
}
// Ensure that the admin user is not deleted
if (hasBaseFunction(BaseFunction.ADMIN)) {
throw new ClientException("ForbiddenError", "The admin user cannot be deleted");
if (hasBaseFunction(BaseFunction.ADMIN) || principal.isGuest()) {
throw new ClientException("ForbiddenError", "This user cannot be deleted");
}
// Find linked data
@ -486,7 +497,7 @@ public class UserResource extends BaseResource {
* @apiName DeleteUserUsername
* @apiGroup User
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied or the admin user cannot be deleted
* @apiError (client) ForbiddenError Access denied or the user cannot be deleted
* @apiError (client) UserNotFound The user does not exist
* @apiPermission admin
* @apiVersion 1.5.0
@ -501,7 +512,12 @@ public class UserResource extends BaseResource {
throw new ForbiddenClientException();
}
checkBaseFunction(BaseFunction.ADMIN);
// Cannot delete the guest user
if (Constants.GUEST_USER_ID.equals(username)) {
throw new ClientException("ForbiddenError", "The guest user cannot be deleted");
}
// Check if the user exists
UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(username);
@ -768,18 +784,21 @@ public class UserResource extends BaseResource {
String authToken = getAuthToken();
JsonArrayBuilder sessions = Json.createArrayBuilder();
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) {
JsonObjectBuilder session = Json.createObjectBuilder()
.add("create_date", authenticationToken.getCreationDate().getTime())
.add("ip", JsonUtil.nullable(authenticationToken.getIp()))
.add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent()));
if (authenticationToken.getLastConnectionDate() != null) {
session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime());
// The guest user cannot see other sessions
if (!principal.isGuest()) {
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) {
JsonObjectBuilder session = Json.createObjectBuilder()
.add("create_date", authenticationToken.getCreationDate().getTime())
.add("ip", JsonUtil.nullable(authenticationToken.getIp()))
.add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent()));
if (authenticationToken.getLastConnectionDate() != null) {
session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime());
}
session.add("current", authenticationToken.getId().equals(authToken));
sessions.add(session);
}
session.add("current", authenticationToken.getId().equals(authToken));
sessions.add(session);
}
JsonObjectBuilder response = Json.createObjectBuilder()
@ -795,7 +814,7 @@ public class UserResource extends BaseResource {
* @apiName DeleteUserSession
* @apiGroup User
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ForbiddenError Access denied or connected as guest
* @apiPermission user
* @apiVersion 1.5.0
*
@ -804,10 +823,10 @@ public class UserResource extends BaseResource {
@DELETE
@Path("session")
public Response deleteSession() {
if (!authenticate()) {
if (!authenticate() || principal.isGuest()) {
throw new ForbiddenClientException();
}
// Get the value of the session token
String authToken = getAuthToken();
@ -830,7 +849,7 @@ public class UserResource extends BaseResource {
* @apiName PostUserEnableTotp
* @apiGroup User
* @apiSuccess {String} secret Secret TOTP seed to initiate the algorithm
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ForbiddenError Access denied or connected as guest
* @apiPermission user
* @apiVersion 1.5.0
*
@ -839,7 +858,7 @@ public class UserResource extends BaseResource {
@POST
@Path("enable_totp")
public Response enableTotp() {
if (!authenticate()) {
if (!authenticate() || principal.isGuest()) {
throw new ForbiddenClientException();
}
@ -866,7 +885,7 @@ public class UserResource extends BaseResource {
* @apiGroup User
* @apiParam {String{1..100}} password Password
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ForbiddenError Access denied or connected as guest
* @apiError (client) ValidationError Validation error
* @apiPermission user
* @apiVersion 1.5.0
@ -877,7 +896,7 @@ public class UserResource extends BaseResource {
@POST
@Path("disable_totp")
public Response disableTotp(@FormParam("password") String password) {
if (!authenticate()) {
if (!authenticate() || principal.isGuest()) {
throw new ForbiddenClientException();
}

View File

@ -7,12 +7,17 @@ module.exports = function(grunt) {
init: ['dist'],
after: ['dist/style.css', 'dist/docs.js', 'dist/share.js', 'dist/less.css', 'dist/app']
},
ngmin: {
ngAnnotate: {
options: {
singleQuotes: true
},
dist: {
expand: true,
cwd: 'src',
src: ['app/**/*.js'],
dest: 'dist'
files: [{
expand: true,
cwd: 'src',
src: ['app/**/*.js'],
dest: 'dist'
}]
}
},
concat: {
@ -110,12 +115,12 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-htmlrefs');
grunt.loadNpmTasks('grunt-css');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-ngmin');
grunt.loadNpmTasks('grunt-ng-annotate');
grunt.loadNpmTasks('grunt-text-replace');
grunt.loadNpmTasks('grunt-apidoc');
// Default tasks.
grunt.registerTask('default', ['clean:init', 'ngmin', 'concat:docs', 'concat:share', 'less', 'concat:css', 'cssmin',
grunt.registerTask('default', ['clean:init', 'ngAnnotate', 'concat:docs', 'concat:share', 'less', 'concat:css', 'cssmin',
'uglify:docs', 'uglify:share', 'copy', 'clean:after', 'cleanempty', 'htmlrefs:index', 'htmlrefs:share', 'replace', 'apidoc']);
};

View File

@ -26,18 +26,33 @@
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- This filter is used to secure URLs -->
<!-- These filters are used to secure URLs -->
<filter>
<filter-name>tokenBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.TokenBasedSecurityFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter>
<filter-name>headerBasedSecurityFilter</filter-name>
<filter-class>com.sismics.util.filter.HeaderBasedSecurityFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>enabled</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>tokenBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>headerBasedSecurityFilter</filter-name>
<url-pattern>/api/*</url-pattern>
</filter-mapping>
<!-- Jersey -->
<servlet>
<servlet-name>JerseyServlet</servlet-name>

View File

@ -0,0 +1,46 @@
The web client and Android application for **Sismics Docs** are only examples
of what is possible with the provided REST API. Everything you see in those apps are
accessible using the API.
This documentation is divided in two parts. The first will get you started on essentials
steps like authentication and the second part is a full reference of every endpoints.
## API URL
The base URL depends on your server. If your instance of Docs is accessible through
`https://docs.mycompany.com`, then the base API URL is `https://docs.mycompany.com/api`.
## Verbs and status codes
The API uses restful verbs.
| Verb | Description |
|---|---|
| `GET` | Select one or more items |
| `PUT` | Create a new item |
| `POST` | Update an item |
| `DELETE` | Delete an item |
Successful calls return a HTTP code 200, anything else if an error.
## Dates
All dates are returned in UNIX timestamp format in milliseconds.
## Authentication
#### **Step 1: [POST /user/login](#api-User-PostUserLogin)**
A call to this endpoint will return a cookie header like this:
```
HTTP Response:
Set-Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4
```
#### **Step 2: Authenticated API calls**
All following API calls must have a cookie header supplying the given token, like this:
```
HTTP Request:
Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4
```
#### **Step 3: [POST /user/logout](#api-User-PostUserLogout)**
A call to this API with a given `auth_token` cookie will make it unusable for other calls.

View File

@ -28,7 +28,11 @@
"App",
"Theme",
"Vocabulary"
]
],
"header": {
"title": "Getting started",
"filename": "header.md"
}
},
"devDependencies": {
"grunt": "^1.0.1",
@ -41,7 +45,7 @@
"grunt-contrib-uglify": "^1.0.1",
"grunt-css": "^0.5.4",
"grunt-htmlrefs": "^0.5.0",
"grunt-ngmin": "0.0.3",
"grunt-ng-annotate": "^2.0.2",
"grunt-text-replace": "^0.4.0",
"protractor": "^3.3.0",
"selenium": "^2.20.0"

View File

@ -106,12 +106,12 @@ angular.module('docs',
}
}
})
.state('settings.theme', {
url: '/theme',
.state('settings.config', {
url: '/config',
views: {
'settings': {
templateUrl: 'partial/docs/settings.theme.html',
controller: 'SettingsTheme'
templateUrl: 'partial/docs/settings.config.html',
controller: 'SettingsConfig'
}
}
})

View File

@ -3,12 +3,24 @@
/**
* Login controller.
*/
angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) {
angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User) {
$scope.codeRequired = false;
/**
* Login.
*/
// Get the app configuration
Restangular.one('app').get().then(function(data) {
$scope.app = data;
});
// Login as guest
$scope.loginAsGuest = function() {
$scope.user = {
username: 'guest',
password: ''
};
$scope.login();
};
// Login
$scope.login = function() {
User.login($scope.user).then(function() {
User.userInfo(true).then(function(data) {

View File

@ -1,9 +1,23 @@
'use strict';
/**
* Settings theme page controller.
* Settings config page controller.
*/
angular.module('docs').controller('SettingsTheme', function($scope, $rootScope, Restangular) {
angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, Restangular) {
// Get the app configuration
Restangular.one('app').get().then(function(data) {
$scope.app = data;
});
// Enable/disable guest login
$scope.changeGuestLogin = function(enabled) {
Restangular.one('app').post('guest_login', {
enabled: enabled
}).then(function() {
$scope.app.guest_login = enabled;
});
};
// Fetch the current theme configuration
Restangular.one('theme').get().then(function(data) {
$scope.theme = data;

View File

@ -60,7 +60,7 @@
<script src="app/docs/controller/settings/Settings.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsDefault.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsAccount.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsTheme.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsConfig.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsSecurity.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsSecurityModalDisableTotp.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/SettingsSession.js" type="text/javascript"></script>
@ -125,12 +125,13 @@
</a>
</li>
<li>
<a href="#/settings/account" title="Logged in as {{ userInfo.username }}">
<a href="{{ userInfo.username == 'guest' ? '#/user/guest' : '#/settings/account' }}"
title="Logged in as {{ userInfo.username }}">
<span class="glyphicon glyphicon-user"></span>
{{ userInfo.username }}
</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/settings.*">
<li ng-class="{active: $uiRoute}" ui-route="/settings.*" ng-show="userInfo.username != 'guest'">
<a href="#/settings/account">
<span class="glyphicon glyphicon-cog"></span> Settings
</a>

View File

@ -46,6 +46,19 @@
</tr>
</thead>
<tbody>
<tr ng-if="!documents">
<td colspan="3" class="text-center">
<img src="img/loader.gif" />
</td>
</tr>
<tr ng-if="totalDocuments == 0">
<td colspan="3" class="text-center">
<span ng-if="search.length == 0">No document in the database</span>
<span ng-if="search.length > 0">No matches for <strong>"{{ search }}"</strong></span>
</td>
</tr>
<tr ng-click="viewDocument(document.id)" ng-repeat="document in documents" ng-class="{ active: $stateParams.id == document.id }">
<td>
{{ document.title }} ({{ document.file_count }})
@ -79,8 +92,11 @@
used on {{ userInfo.storage_quota / 1000000 | number: 0 }}MB
</div>
<div class="text-right">
{{ totalDocuments }} document{{ totalDocuments > 1 ? 's' : '' }} found
<div class="text-right" >
<span ng-if="totalDocuments">
{{ totalDocuments }} document{{ totalDocuments > 1 ? 's' : '' }} found
</span>
<span ng-if="!totalDocuments">&nbsp;</span>
</div>
</div>
</div>

View File

@ -35,6 +35,12 @@
<button type="submit" class="btn btn-primary btn-block" ng-click="login()">
<span class="glyphicon glyphicon-ok"></span> Sign in
</button>
<p class="text-center lead" ng-if="app.guest_login">&nbsp;</p>
<button type="submit" class="btn btn-default btn-block" ng-if="app.guest_login" ng-click="loginAsGuest()">
<span class="glyphicon glyphicon-user"></span> Login as guest
</button>
</form>
</div>
</div>

View File

@ -1,3 +1,18 @@
<h1>
Guest <small>access</small>
<span class="label" ng-class="{ 'label-success': app.guest_login, 'label-danger': !app.guest_login }">
{{ app.guest_login ? 'Enabled' : 'Disabled' }}
</span>
</h1>
<p>
Guest access is a mode where anyone can access {{ appName }} without password.<br/>
Like a normal user, the guest user can only access its documents and those accessible through permissions.<br/>
</p>
<div ng-if="app">
<button ng-if="!app.guest_login" class="btn btn-primary" ng-click="changeGuestLogin(true)">Enable guest access</button>
<button ng-if="app.guest_login" class="btn btn-danger" ng-click="changeGuestLogin(false)">Disable guest access</button>
</div>
<h1>Theme <small>customization</small></h1>
<form class="form-horizontal" name="editColorForm" novalidate>
<div class="form-group">

View File

@ -1,4 +1,4 @@
<h1>Groups <small>management</small> <a class="btn btn-primary" href="#/settings/group/add">Add</a></h1>
<h1>Groups <small>management</small> <a class="btn btn-primary" href="#/settings/group/add">Add a group</a></h1>
<div class="row">
<div class="col-md-4 well">

View File

@ -9,14 +9,14 @@
</ul>
</div>
<div class="panel panel-default">
<div class="panel-heading" ng-show="isAdmin"><strong>General settings</strong></div>
<div class="panel panel-default" ng-show="isAdmin">
<div class="panel-heading"><strong>General settings</strong></div>
<ul class="list-group">
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">Users</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">Groups</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">Vocabularies</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/theme" href="#/settings/theme">Theme</a>
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">Server logs</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">Users</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">Groups</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">Vocabularies</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/config" href="#/settings/config">Configuration</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">Server logs</a>
</ul>
</div>
</div>

View File

@ -13,10 +13,12 @@
<td>{{ session.create_date | date: 'yyyy-MM-dd HH:mm' }}</td>
<td>{{ session.last_connection_date | date: 'yyyy-MM-dd HH:mm' }}</td>
<td title="{{ session.user_agent }}">{{ session.ip }}</td>
<td><span ng-show="session.current" class="glyphicon glyphicon-ok"></span></td>
<td>
<span ng-show="session.current" class="glyphicon glyphicon-ok" title="This is the current session"></span>
</td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-warning" ng-click="deleteSession()">Clear all other sessions</button>
<button type="submit" class="btn btn-warning" title="All other devices connected to this account will be disconnected" ng-click="deleteSession()">Clear all other sessions</button>
</div>

View File

@ -62,7 +62,7 @@
<span class="help-block" ng-show="editUserForm.storage_quota.$error.pattern">Number required</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !editUserForm.password.$valid, success: editUserForm.password.$valid }">
<div class="form-group" ng-if="user.username != 'guest'" ng-class="{ 'has-error': !editUserForm.password.$valid, success: editUserForm.password.$valid }">
<label class="col-sm-2 control-label" for="inputPassword">Password</label>
<div class="col-sm-7">
@ -76,7 +76,7 @@
<span class="help-block" ng-show="editUserForm.password.$error.maxlength">Too long</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
<div class="form-group" ng-if="user.username != 'guest'" ng-class="{ 'has-error': !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
<label class="col-sm-2 -label" for="inputPasswordConfirm">Password (confirm)</label>
<div class="col-sm-7">
@ -94,7 +94,7 @@
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'Edit' : 'Add' }}
</button>
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit() && user.username != 'guest'">
<span class="glyphicon glyphicon-trash"></span> Delete
</button>
</div>

View File

@ -1,4 +1,4 @@
<h1>Users <small>management</small> <a class="btn btn-primary" href="#/settings/user/add">Add</a></h1>
<h1>Users <small>management</small> <a class="btn btn-primary" href="#/settings/user/add">Add a user</a></h1>
<div class="row">
<div class="col-md-4 well">

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=9
db.version=10

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=9
db.version=10

View File

@ -1,5 +1,13 @@
package com.sismics.docs.rest;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.AclDao;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sismics.util.jpa.EMF;
import org.junit.Assert;
import org.junit.Test;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.persistence.EntityManager;
@ -9,15 +17,6 @@ import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.AclDao;
import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.jpa.EMF;
import org.junit.Assert;
import org.junit.Test;
import com.sismics.util.filter.TokenBasedSecurityFilter;
/**
* Test the app resource.
@ -35,17 +34,15 @@ public class TestAppResource extends BaseJerseyTest {
// Check the application info
JsonObject json = target().path("/app").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.get(JsonObject.class);
String currentVersion = json.getString("current_version");
Assert.assertNotNull(currentVersion);
String minVersion = json.getString("min_version");
Assert.assertNotNull(minVersion);
Assert.assertNotNull(json.getString("current_version"));
Assert.assertNotNull(json.getString("min_version"));
Long freeMemory = json.getJsonNumber("free_memory").longValue();
Assert.assertTrue(freeMemory > 0);
Long totalMemory = json.getJsonNumber("total_memory").longValue();
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
Assert.assertFalse(json.getBoolean("guest_login"));
// Rebuild Lucene index
Response response = target().path("/app/batch/reindex").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
@ -127,4 +124,69 @@ public class TestAppResource extends BaseJerseyTest {
Long date4 = logs.getJsonObject(9).getJsonNumber("date").longValue();
Assert.assertTrue(date3 >= date4);
}
/**
* Test the guest login.
*/
@Test
public void testGuestLogin() {
// Login admin
String adminToken = clientUtil.login("admin", "admin", false);
// Try to login as guest
Response response = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "guest")));
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
// Enable guest login
target().path("/app/guest_login").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form()
.param("enabled", "true")), JsonObject.class);
// Login as guest
String guestToken = clientUtil.login("guest", "", false);
// Guest cannot delete himself
response = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.delete();
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
// Guest cannot see opened sessions
JsonObject json = target().path("/user/session").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.get(JsonObject.class);
Assert.assertEquals(0, json.getJsonArray("sessions").size());
// Guest cannot delete opened sessions
response = target().path("/user/session").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.delete();
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
// Guest cannot enable TOTP
response = target().path("/user/enable_totp").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.post(Entity.form(new Form()));
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
// Guest cannot disable TOTP
response = target().path("/user/disable_totp").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.post(Entity.form(new Form()));
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
// Guest cannot update itself
response = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.post(Entity.form(new Form()));
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
// Guest can see its documents
target().path("/document/list").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
.get(JsonObject.class);
}
}

View File

@ -6,6 +6,7 @@ import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import com.sismics.util.filter.HeaderBasedSecurityFilter;
import org.junit.Assert;
import org.apache.commons.lang.StringUtils;
@ -28,7 +29,7 @@ public class TestSecurity extends BaseJerseyTest {
clientUtil.createUser("testsecurity");
// Changes a user's email KO : the user is not connected
Response response = target().path("/user/update").request()
Response response = target().path("/user").request()
.post(Entity.form(new Form().param("email", "testsecurity2@docs.com")));
Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus()));
JsonObject json = response.readEntity(JsonObject.class);
@ -73,4 +74,29 @@ public class TestSecurity extends BaseJerseyTest {
// User testsecurity logs out
clientUtil.logout(testSecurityToken);
}
@Test
public void testHeaderBasedAuthentication() {
clientUtil.createUser("header_auth_test");
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target()
.path("/user/session")
.request()
.get()
.getStatus());
Assert.assertEquals(Status.OK.getStatusCode(), target()
.path("/user/session")
.request()
.header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "header_auth_test")
.get()
.getStatus());
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target()
.path("/user/session")
.request()
.header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "idontexist")
.get()
.getStatus());
}
}

View File

@ -4,7 +4,7 @@ hibernate.connection.username=sa
hibernate.connection.password=
hibernate.hbm2ddl.auto=
hibernate.dialect=org.hibernate.dialect.HSQLDialect
hibernate.show_sql=true
hibernate.show_sql=false
hibernate.format_sql=false
hibernate.max_fetch_depth=5
hibernate.cache.use_second_level_cache=false

View File

@ -1,10 +1,10 @@
log4j.rootCategory=DEBUG, CONSOLE, MEMORY
log4j.rootCategory=INFO, CONSOLE, MEMORY
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{DATE} %p %l %m %n
log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender
log4j.appender.MEMORY.size=1000
log4j.logger.com.sismics=DEBUG
log4j.logger.com.sismics=INFO
log4j.logger.org.hibernate=INFO
log4j.logger.org.apache.pdfbox=INFO