diff options
48 files changed, 702 insertions, 96 deletions
diff --git a/CHANGELOG b/CHANGELOG index 8dc23cf175f..8a24bf412d2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,12 +18,17 @@ v 7.14.0 (unreleased) - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu) - Expire Rails cache entries after two weeks to prevent endless Redis growth - Add support for destroying project milestones (Stan Hu) + - Add fetch command to the MR page. + - Allow custom backup archive permissions - Add fetch command to the MR page - Add project star and fork count, group avatar URL and user/group web URL attributes to API - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. - Add fetch command to the MR page. + - Add ability to manage user email addresses via the API. + - Show buttons to add license, changelog and contribution guide if they're missing. - Disabled autocapitalize and autocorrect on login field (Daryl Chan) - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis) + - Remove redis-store TTL monkey patch v 7.13.2 - Fix randomly failed spec diff --git a/Gemfile.lock b/Gemfile.lock index 44365017edc..58622f2ac10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -508,7 +508,7 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.3.2) - redis (3.1.0) + redis (3.2.1) redis-actionpack (4.0.0) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -525,7 +525,7 @@ GEM redis-actionpack (~> 4) redis-activesupport (~> 4) redis-store (~> 1.1.0) - redis-store (1.1.4) + redis-store (1.1.6) redis (>= 2.2) request_store (1.0.5) rerun (0.10.0) diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index d64e79170b9..7beef1845ef 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -70,7 +70,7 @@ font-family: $monospace_font; white-space: pre; word-wrap: normal; - padding: 0; + padding: 1px 2px; } kbd { diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 2db4213159a..34b4ee3e17e 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -38,6 +38,10 @@ code { } } +a > code { + color: $link-color; +} + /** * Wiki typography * diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 5f415f2d78f..21d958db80c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -297,6 +297,15 @@ table.table.protected-branches-list tr.no-border { ul.nav-pills { display:inline-block; } li { display:inline; } a { float:left; } + + li.missing a { + color: #bbb; + border: 1px dashed #ccc; + + &:hover { + background-color: #FAFAFA; + } + } } pre.light-well { diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a675a292432..f5b78533896 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -184,7 +184,43 @@ module ProjectsHelper end end - def contribution_guide_url(project) + def add_contribution_guide_path(project) + if project && !project.repository.contribution_guide + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "CONTRIBUTING.md", + commit_message: "Add contribution guide" + ) + end + end + + def add_changelog_path(project) + if project && !project.repository.changelog + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "CHANGELOG", + commit_message: "Add changelog" + ) + end + end + + def add_license_path(project) + if project && !project.repository.license + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "LICENSE", + commit_message: "Add license" + ) + end + end + + def contribution_guide_path(project) if project && contribution_guide = project.repository.contribution_guide namespace_project_blob_path( project.namespace, @@ -195,7 +231,7 @@ module ProjectsHelper end end - def changelog_url(project) + def changelog_path(project) if project && changelog = project.repository.changelog namespace_project_blob_path( project.namespace, @@ -206,7 +242,7 @@ module ProjectsHelper end end - def license_url(project) + def license_path(project) if project && license = project.repository.license namespace_project_blob_path( project.namespace, @@ -217,7 +253,7 @@ module ProjectsHelper end end - def version_url(project) + def version_path(project) if project && version = project.repository.version namespace_project_blob_path( project.namespace, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index fee52694099..6d1ad82a262 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -14,13 +14,14 @@ # default_branch_protection :integer default(2) # twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null -# session_expire_delay :integer default(10080), not null # default_project_visibility :integer # default_snippet_visibility :integer # restricted_signup_domains :text -# user_oauth_applications :bool default(TRUE) +# user_oauth_applications :boolean default(TRUE) # after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null # class ApplicationSetting < ActiveRecord::Base diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 967ffd46db0..0ed0dd98a59 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -1,3 +1,17 @@ +# == Schema Information +# +# Table name: audit_events +# +# id :integer not null, primary key +# author_id :integer not null +# type :string(255) not null +# entity_id :integer not null +# entity_type :string(255) not null +# details :text +# created_at :datetime +# updated_at :datetime +# + class AuditEvent < ActiveRecord::Base serialize :details, Hash diff --git a/app/models/project.rb b/app/models/project.rb index 0921fdfe9b4..3dc1729e812 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -21,12 +21,13 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # require 'carrierwave/orm/activerecord' diff --git a/app/models/security_event.rb b/app/models/security_event.rb index d131c11cb6c..68c00adad59 100644 --- a/app/models/security_event.rb +++ b/app/models/security_event.rb @@ -1,2 +1,16 @@ +# == Schema Information +# +# Table name: audit_events +# +# id :integer not null, primary key +# author_id :integer not null +# type :string(255) not null +# entity_id :integer not null +# entity_type :string(255) not null +# details :text +# created_at :datetime +# updated_at :datetime +# + class SecurityEvent < AuditEvent end diff --git a/app/models/user.rb b/app/models/user.rb index 4a10520b209..2beefb1d614 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -57,6 +57,7 @@ # otp_backup_codes :text # public_email :string(255) default(""), not null # dashboard :integer default(0) +# project_view :integer default(0) # require 'carrierwave/orm/activerecord' diff --git a/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml index 423fcd48e25..1c565bae80a 100644 --- a/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml +++ b/app/views/projects/merge_requests/widget/open/_missing_branch.html.haml @@ -6,9 +6,11 @@ %span.label.label-inverse= @merge_request.source_branch does not exist in %span.label.label-info= @merge_request.source_project_path + %br + %strong Please close this merge request and open a new merge request to change source branches. - else %span.label.label-inverse= @merge_request.target_branch does not exist in %span.label.label-info= @merge_request.target_project_path - %br - %strong Please close this merge request or change branches with existing one + %br + %strong Please close this merge request or change to another target branch. diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 769dd68f089..4577b84ab89 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -22,19 +22,34 @@ %li = link_to namespace_project_tags_path(@project.namespace, @project) do = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag') + - if @repository.changelog %li - = link_to changelog_url(@project) do + = link_to changelog_path(@project) do Changelog - if @repository.license %li - = link_to license_url(@project) do + = link_to license_path(@project) do License - if @repository.contribution_guide %li - = link_to contribution_guide_url(@project) do + = link_to contribution_guide_path(@project) do Contribution guide + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_changelog_path(@project) do + Add Changelog + - unless @repository.license + %li.missing + = link_to add_license_path(@project) do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_contribution_guide_path(@project) do + Add Contribution guide + - if @project.archived? .text-warning.center.prepend-top-20 %p diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 8cc0b517cd2..ac8c1936c9e 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -100,7 +100,7 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) .form-actions - - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? + - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted? %p Please review the %strong #{link_to 'guidelines for contribution', guide_url} diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 41a01e7703c..56770335ddc 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -256,6 +256,7 @@ production: &base ## Backup settings backup: path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/) + # archive_permissions: 0640 # Permissions for the resulting backup.tar file (default: 0600) # keep_time: 604800 # default: 0 (forever) (in seconds) # upload: # # Fog storage connection settings, see http://fog.io/storage/ . @@ -347,6 +348,8 @@ test: # user: YOUR_USERNAME satellites: path: tmp/tests/gitlab-satellites/ + backup: + path: tmp/tests/backups gitlab_shell: path: tmp/tests/gitlab-shell/ repos_path: tmp/tests/repositories/ diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7b5d488f59e..bd76c918485 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -170,6 +170,7 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] diff --git a/config/initializers/redis-store-fix-expiry.rb b/config/initializers/redis-store-fix-expiry.rb deleted file mode 100644 index fce0a135330..00000000000 --- a/config/initializers/redis-store-fix-expiry.rb +++ /dev/null @@ -1,44 +0,0 @@ -# Monkey-patch Redis::Store to make 'setex' and 'expire' work with namespacing - -module Gitlab - class Redis - class Store - module Namespace - # Redis::Store#setex in redis-store 1.1.4 does not respect namespaces; - # this new method does. - def setex(key, expires_in, value, options=nil) - namespace(key) { |key| super(key, expires_in, value) } - end - - # Redis::Store#expire in redis-store 1.1.4 does not respect namespaces; - # this new method does. - def expire(key, expires_in) - namespace(key) { |key| super(key, expires_in) } - end - - private - - # Our new definitions of #setex and #expire above assume that the - # #namespace method exists. Because we cannot be sure of that, we - # re-implement the #namespace method from Redis::Store::Namespace so - # that it is available for all Redis::Store instances, whether they use - # namespacing or not. - # - # Based on lib/redis/store/namespace.rb L49-51 (redis-store 1.1.4) - def namespace(key) - if @namespace - yield interpolate(key) - else - # This Redis::Store instance does not use a namespace so we should - # just pass through the key. - yield key - end - end - end - end - end -end - -Redis::Store.class_eval do - include Gitlab::Redis::Store::Namespace -end diff --git a/doc/api/users.md b/doc/api/users.md index 5dca77b5c7b..7ba2db248ff 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -397,6 +397,138 @@ Parameters: Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found. +## List emails + +Get a list of currently authenticated user's emails. + +``` +GET /user/emails +``` + +```json +[ + { + "id": 1, + "email": "email@example.com" + }, + { + "id": 3, + "email": "email2@example.com" + } +] +``` + +Parameters: + +- **none** + +## List emails for user + +Get a list of a specified user's emails. Available only for admin + +``` +GET /users/:uid/emails +``` + +Parameters: + +- `uid` (required) - id of specified user + +## Single email + +Get a single email. + +``` +GET /user/emails/:id +``` + +Parameters: + +- `id` (required) - email ID + +```json +{ + "id": 1, + "email": "email@example.com" +} +``` + +## Add email + +Creates a new email owned by the currently authenticated user. + +``` +POST /user/emails +``` + +Parameters: + +- `email` (required) - email address + +```json +{ + "id": 4, + "email": "email@example.com" +} +``` + +Will return created email with status `201 Created` on success. If an +error occurs a `400 Bad Request` is returned with a message explaining the error: + +```json +{ + "message": { + "email": [ + "has already been taken" + ] + } +} +``` + +## Add email for user + +Create new email owned by specified user. Available only for admin + +``` +POST /users/:id/emails +``` + +Parameters: + +- `id` (required) - id of specified user +- `email` (required) - email address + +Will return created email with status `201 Created` on success, or `404 Not found` on fail. + +## Delete email for current user + +Deletes email owned by currently authenticated user. +This is an idempotent function and calling it on a email that is already deleted +or not available results in `200 OK`. + +``` +DELETE /user/emails/:id +``` + +Parameters: + +- `id` (required) - email ID + +## Delete email for given user + +Deletes email owned by a specified user. Available only for admin. + +``` +DELETE /users/:uid/emails/:id +``` + +Parameters: + +- `uid` (required) - id of specified user +- `id` (required) - email ID + +Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found. + ## Block user Blocks the specified user. Available only for admin. diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index 538894f5848..9491f8c91fe 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -19,3 +19,5 @@ Step-by-step guides on the basics of working with Git and GitLab. * [Fork a project](fork-project.md) * [Add a file](add-file.md) + +* [Create a Merge Request](add-merge-request.md) diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md index e7c441e7698..57136ac5c39 100644 --- a/doc/gitlab-basics/add-file.md +++ b/doc/gitlab-basics/add-file.md @@ -2,7 +2,7 @@ You can create a file in your [shell](command-line-commands.md) or in GitLab. -To create a file in GitLab, sign in to [GitLab.com](https://gitlab.com). +To create a file in GitLab, sign in to GitLab. Select a project on the right side of your screen: diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md new file mode 100644 index 00000000000..236b4248ea2 --- /dev/null +++ b/doc/gitlab-basics/add-merge-request.md @@ -0,0 +1,42 @@ +# How to create a merge request + +Merge Requests are useful to integrate separate changes that you've made to a project, on different branches. + +To create a new Merge Request, sign in to GitLab. + +Go to the project where you'd like to merge your changes: + +![Select a project](basicsimages/select_project.png) + +Click on "Merge Requests" on the left side of your screen: + +![Merge requests](basicsimages/merge_requests.png) + +Click on "+ new Merge Request" on the right side of the screen: + +![New Merge Request](basicsimages/new_merge_request.png) + +Select a source branch or branch: + +![Select a branch](basicsimages/select_branch.png) + +Click on the "compare branches" button: + +![Compare branches](basicsimages/compare_branches.png) + +Add a title and a description to your Merge Request: + +![Add a title and description](basicsimages/title_description_mr.png) + +Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button: + +![Add a new merge request](basicsimages/add_new_merge_request.png) + +Your Merge Request will be ready to be approved and published. + +### Note + +After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen. +You may automatically create a Merge Request from your recently created branch when clicking on this button: + +![Automatic MR button](basicsimages/button-create-mr.png) diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png Binary files differnew file mode 100644 index 00000000000..457af459bb9 --- /dev/null +++ b/doc/gitlab-basics/basicsimages/button-create-mr.png diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index a8223a9b161..b03cca4029c 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -2,7 +2,7 @@ ## Start working on your project -In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to [GitLab.com](https://gitlab.com). +In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab. When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen. diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md index 8e168395ff7..f80ae62e442 100644 --- a/doc/gitlab-basics/create-group.md +++ b/doc/gitlab-basics/create-group.md @@ -2,7 +2,7 @@ ## Create a group -Your projects in [GitLab.com](https://gitlab.com) can be organized in 2 different ways: +Your projects in GitLab can be organized in 2 different ways: under your own namespace for single projects, such as ´your-name/project-1'; or under groups. If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects. diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 90d40cb6c51..b545d62549d 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,6 +1,6 @@ # How to create a project in GitLab -To create a new project, sign in to [GitLab.com](https://gitlab.com). +To create a new project, sign in to GitLab. Go to your Dashboard and click on "new project" on the right side of your screen. diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md index dcd3e6ffb31..c8a73feb028 100644 --- a/doc/gitlab-basics/create-your-ssh-keys.md +++ b/doc/gitlab-basics/create-your-ssh-keys.md @@ -6,7 +6,7 @@ You need to connect your computer to your GitLab account through SSH Keys. They Create an account on GitLab. Sign up and check your email for your confirmation link. -After you confirm, go to [GitLab.com](https://about.gitlab.com/) and sign in to your account. +After you confirm, go to GitLab and sign in to your account. ## Add your SSH Key diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md index 5173aae2c0f..5f8b81ea919 100644 --- a/doc/gitlab-basics/fork-project.md +++ b/doc/gitlab-basics/fork-project.md @@ -6,7 +6,7 @@ publishing or not, without affecting your original project. It takes just a few steps to fork a project in GitLab. -Sign in to [gitlab.com](https://gitlab.com). +Sign in to GitLab. Select a project on the right side of your screen: diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 5b1c6c1cd46..b2ceda025c0 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -1,6 +1,6 @@ # Start using Git on the command line -If you want to start using a Git and GitLab, make sure that you have created an account on [GitLab.com](https://about.gitlab.com/). +If you want to start using a Git and GitLab, make sure that you have created an account on GitLab. ## Open a shell diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index e81432c600f..7a6a1958445 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -6,6 +6,9 @@ If a user is both in a project group and in the project itself, the highest perm If a user is a GitLab administrator they receive all permissions. +To add or import a user, you can follow the [project users and members +documentation](doc/workflow/add-user/add-user.md). + ## Project | Action | Guest | Reporter | Developer | Master | Owner | diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 770b7a70fe0..a8dc5c24df2 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -7,3 +7,4 @@ - [User management](user_management.md) - [Web hooks](web_hooks.md) - [Import](import.md) of git repositories in bulk +- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
\ No newline at end of file diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 36ab2b91959..c095ae48dc3 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -148,6 +148,23 @@ with the name of your bucket: } ``` +## Backup archive permissions + +The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default. +This is meant to avoid other system users reading GitLab's data. +If you need the backup archives to have different permissions you can use the 'archive_permissions' setting. + +``` +# In /etc/gitlab/gitlab.rb, for omnibus packages +gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable +``` + +``` +# In gitlab.yml, for installations from source: + backup: + archive_permissions: 0644 # Makes the backup archives world-readable +``` + ## Storing configuration files Please be informed that a backup does not store your configuration @@ -232,7 +249,7 @@ Deleting tmp directories...[DONE] We will assume that you have installed GitLab from an omnibus package and run `sudo gitlab-ctl reconfigure` at least once. -First make sure your backup tar file is in `/var/opt/gitlab/backups`. +First make sure your backup tar file is in `/var/opt/gitlab/backups` (or wherever `gitlab_rails['backup_path']` points to). ```shell sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ @@ -352,4 +369,4 @@ For more information see similar questions on postgresql issue tracker[here](htt ## Note This documentation is for GitLab CE. -We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com. +We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
\ No newline at end of file diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 1f39d02bdf3..3915198ad2a 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -10,6 +10,7 @@ - [Notification emails](notifications.md) - [Project Features](project_features.md) - [Project forking workflow](forking_workflow.md) +- [Project users](add-user/add-user.md) - [Protected branches](protected_branches.md) - [Web Editor](web_editor.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md new file mode 100644 index 00000000000..8c9b4f72631 --- /dev/null +++ b/doc/workflow/add-user/add-user.md @@ -0,0 +1,25 @@ +# Project users + +You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per project. + +Here's how to add or import users to your projects. + +You should have 'master' or 'owner' permissions to add or import a new user +to your project. + +To add or import a user, go to your project and click on "Members" on the left side of your screen: + +![Members](images/members.png) + +Select "Add members" or "Import members" on the right side of your screen: + +![Add or Import](images/add-members.png) + +If you are adding a user, select the user and the [permission level](doc/permissions/permissions.md) that you'd like to +give the user: + +![Add or Import](images/new-member.png) + +If you are importing a user, follow the steps to select the project where you'd like to import the user from: + +![Add or Import](images/select-project.png) diff --git a/doc/workflow/add-user/images/add-members.png b/doc/workflow/add-user/images/add-members.png Binary files differnew file mode 100644 index 00000000000..2805c5764a5 --- /dev/null +++ b/doc/workflow/add-user/images/add-members.png diff --git a/doc/workflow/add-user/images/members.png b/doc/workflow/add-user/images/members.png Binary files differnew file mode 100644 index 00000000000..f1797b95f67 --- /dev/null +++ b/doc/workflow/add-user/images/members.png diff --git a/doc/workflow/add-user/images/new-member.png b/doc/workflow/add-user/images/new-member.png Binary files differnew file mode 100644 index 00000000000..d500daea56e --- /dev/null +++ b/doc/workflow/add-user/images/new-member.png diff --git a/doc/workflow/add-user/images/select-project.png b/doc/workflow/add-user/images/select-project.png Binary files differnew file mode 100644 index 00000000000..dd3844edff8 --- /dev/null +++ b/doc/workflow/add-user/images/select-project.png diff --git a/lib/api/entities.rb b/lib/api/entities.rb index dcfd7a8e1a7..b5556682449 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -35,6 +35,10 @@ module API expose :private_token end + class Email < Grape::Entity + expose :id, :email + end + class Hook < Grape::Entity expose :id, :url, :created_at end diff --git a/lib/api/users.rb b/lib/api/users.rb index c468371d3d4..ee29f952246 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -131,11 +131,11 @@ module API # Add ssh key to a specified user. Only available to admin users. # # Parameters: - # id (required) - The ID of a user - # key (required) - New SSH Key - # title (required) - New SSH Key's title + # id (required) - The ID of a user + # key (required) - New SSH Key + # title (required) - New SSH Key's title # Example Request: - # POST /users/:id/keys + # POST /users/:id/keys post ":id/keys" do authenticated_as_admin! required_attributes! [:title, :key] @@ -153,9 +153,9 @@ module API # Get ssh keys of a specified user. Only available to admin users. # # Parameters: - # uid (required) - The ID of a user + # uid (required) - The ID of a user # Example Request: - # GET /users/:uid/keys + # GET /users/:uid/keys get ':uid/keys' do authenticated_as_admin! user = User.find_by(id: params[:uid]) @@ -185,6 +185,65 @@ module API end end + # Add email to a specified user. Only available to admin users. + # + # Parameters: + # id (required) - The ID of a user + # email (required) - Email address + # Example Request: + # POST /users/:id/emails + post ":id/emails" do + authenticated_as_admin! + required_attributes! [:email] + + user = User.find(params[:id]) + attrs = attributes_for_keys [:email] + email = user.emails.new attrs + if email.save + NotificationService.new.new_email(email) + present email, with: Entities::Email + else + render_validation_error!(email) + end + end + + # Get emails of a specified user. Only available to admin users. + # + # Parameters: + # uid (required) - The ID of a user + # Example Request: + # GET /users/:uid/emails + get ':uid/emails' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + not_found!('User') unless user + + present user.emails, with: Entities::Email + end + + # Delete existing email of a specified user. Only available to admin + # users. + # + # Parameters: + # uid (required) - The ID of a user + # id (required) - Email ID + # Example Request: + # DELETE /users/:uid/emails/:id + delete ':uid/emails/:id' do + authenticated_as_admin! + user = User.find_by(id: params[:uid]) + not_found!('User') unless user + + begin + email = user.emails.find params[:id] + email.destroy + + user.update_secondary_emails! + rescue ActiveRecord::RecordNotFound + not_found!('Email') + end + end + # Delete user. Available only for admin # # Example Request: @@ -289,6 +348,58 @@ module API rescue end end + + # Get currently authenticated user's emails + # + # Example Request: + # GET /user/emails + get "emails" do + present current_user.emails, with: Entities::Email + end + + # Get single email owned by currently authenticated user + # + # Example Request: + # GET /user/emails/:id + get "emails/:id" do + email = current_user.emails.find params[:id] + present email, with: Entities::Email + end + + # Add new email to currently authenticated user + # + # Parameters: + # email (required) - Email address + # Example Request: + # POST /user/emails + post "emails" do + required_attributes! [:email] + + attrs = attributes_for_keys [:email] + email = current_user.emails.new attrs + if email.save + NotificationService.new.new_email(email) + present email, with: Entities::Email + else + render_validation_error!(email) + end + end + + # Delete existing email of currently authenticated user + # + # Parameters: + # id (required) - EMail ID + # Example Request: + # DELETE /user/emails/:id + delete "emails/:id" do + begin + email = current_user.emails.find params[:id] + email.destroy + + current_user.update_secondary_emails! + rescue + end + end end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 9ae4b346436..13c68d9354f 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -18,14 +18,14 @@ module Backup # create archive $progress.print "Creating backup archive: #{tar_file} ... " - orig_umask = File.umask(0077) - if Kernel.system('tar', '-cf', tar_file, *backup_contents) + # Set file permissions on open to prevent chmod races. + tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} + if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red abort 'Backup failed' end - File.umask(orig_umask) upload(tar_file) end diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 092a920a0c4..3f92212243d 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -47,7 +47,7 @@ module Rouge @lineanchors = lineanchors @lineanchorsid = lineanchorsid @anchorlinenos = anchorlinenos - @inline_theme = Theme.find(@inline_theme).new if @inline_theme.is_a?(String) + @inline_theme = Theme.find(inline_theme).new if inline_theme.is_a?(String) end def render(tokens) diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 102678a1d74..1d500a11ad7 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -21,12 +21,13 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # FactoryGirl.define do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index d648f4078be..bc14ff98fd8 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -14,11 +14,14 @@ # default_branch_protection :integer default(2) # twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null -# session_expire_delay :integer default(10080), not null # default_project_visibility :integer # default_snippet_visibility :integer # restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null # require 'spec_helper' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1ffd92b9bd9..5d40754d59d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -21,12 +21,13 @@ # import_url :string(255) # visibility_level :integer default(0), not null # archived :boolean default(FALSE), not null +# avatar :string(255) # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) -# avatar :string(255) +# commit_count :integer default(0) # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 76f16323e2f..922e9ebf844 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -57,6 +57,7 @@ # otp_backup_codes :text # public_email :string(255) default(""), not null # dashboard :integer default(0) +# project_view :integer default(0) # require 'spec_helper' diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c4dd1f76cf2..f2aa369985e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -6,6 +6,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } describe "GET /users" do context "when unauthenticated" do @@ -384,6 +385,87 @@ describe API::API, api: true do end end + describe "POST /users/:id/emails" do + before { admin } + + it "should not create invalid email" do + post api("/users/#{user.id}/emails", admin), {} + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "email" not given') + end + + it "should create email" do + email_attrs = attributes_for :email + expect do + post api("/users/#{user.id}/emails", admin), email_attrs + end.to change{ user.emails.count }.by(1) + end + end + + describe 'GET /user/:uid/emails' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + get api("/users/#{user.id}/emails") + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'should return 404 for non-existing user' do + get api('/users/999999/emails', admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'should return array of emails' do + user.emails << email + user.save + get api("/users/#{user.id}/emails", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['email']).to eq(email.email) + end + end + end + + describe 'DELETE /user/:uid/emails/:id' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + delete api("/users/#{user.id}/emails/42") + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'should delete existing email' do + user.emails << email + user.save + expect do + delete api("/users/#{user.id}/emails/#{email.id}", admin) + end.to change { user.emails.count }.by(-1) + expect(response.status).to eq(200) + end + + it 'should return 404 error if user not found' do + user.emails << email + user.save + delete api("/users/999999/emails/#{email.id}", admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'should return 404 error if email not foud' do + delete api("/users/#{user.id}/emails/42", admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Email Not Found') + end + end + end + describe "DELETE /users/:id" do before { admin } @@ -528,6 +610,95 @@ describe API::API, api: true do end end + describe "GET /user/emails" do + context "when unauthenticated" do + it "should return authentication error" do + get api("/user/emails") + expect(response.status).to eq(401) + end + end + + context "when authenticated" do + it "should return array of emails" do + user.emails << email + user.save + get api("/user/emails", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first["email"]).to eq(email.email) + end + end + end + + describe "GET /user/emails/:id" do + it "should return single email" do + user.emails << email + user.save + get api("/user/emails/#{email.id}", user) + expect(response.status).to eq(200) + expect(json_response["email"]).to eq(email.email) + end + + it "should return 404 Not Found within invalid ID" do + get api("/user/emails/42", user) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') + end + + it "should return 404 error if admin accesses user's email" do + user.emails << email + user.save + admin + get api("/user/emails/#{email.id}", admin) + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + describe "POST /user/emails" do + it "should create email" do + email_attrs = attributes_for :email + expect do + post api("/user/emails", user), email_attrs + end.to change{ user.emails.count }.by(1) + expect(response.status).to eq(201) + end + + it "should return a 401 error if unauthorized" do + post api("/user/emails"), email: 'some email' + expect(response.status).to eq(401) + end + + it "should not create email with invalid email" do + post api("/user/emails", user), {} + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "email" not given') + end + end + + describe "DELETE /user/emails/:id" do + it "should delete existed email" do + user.emails << email + user.save + expect do + delete api("/user/emails/#{email.id}", user) + end.to change{user.emails.count}.by(-1) + expect(response.status).to eq(200) + end + + it "should return success if email ID not found" do + delete api("/user/emails/42", user) + expect(response.status).to eq(200) + end + + it "should return 401 error if unauthorized" do + user.emails << email + user.save + delete api("/user/emails/#{email.id}") + expect(response.status).to eq(401) + end + end + describe 'PUT /user/:id/block' do before { admin } it 'should block existing user' do diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index dab4535e2c7..8dc687c3580 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -35,6 +35,7 @@ module TestEnv clean_test_path FileUtils.mkdir_p(repos_path) + FileUtils.mkdir_p(backup_path) # Setup GitLab shell for test instance setup_gitlab_shell @@ -127,6 +128,10 @@ module TestEnv Gitlab.config.gitlab_shell.repos_path end + def backup_path + Gitlab.config.backup.path + end + def copy_forked_repo_with_submodules(project) base_repo_path = File.expand_path(forked_repo_path_bare) target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git") diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index cdcfeba8d1f..23f322e0a62 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -15,6 +15,12 @@ describe 'gitlab:app namespace rake task' do Rake.application.invoke_task task_name end + def reenable_backup_sub_tasks + %w{db repo uploads}.each do |subtask| + Rake::Task["gitlab:backup:#{subtask}:create"].reenable + end + end + describe 'backup_restore' do before do # avoid writing task output to spec progress @@ -60,26 +66,47 @@ describe 'gitlab:app namespace rake task' do Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) end - before :all do - # Record the existing backup tars so we don't touch them - existing_tars = tars_glob + def create_backup + FileUtils.rm tars_glob # Redirect STDOUT and run the rake task orig_stdout = $stdout $stdout = StringIO.new + reenable_backup_sub_tasks run_rake_task('gitlab:backup:create') + reenable_backup_sub_tasks $stdout = orig_stdout - @backup_tar = (tars_glob - existing_tars).first + @backup_tar = tars_glob.first end - after :all do + before do + create_backup + end + + after do FileUtils.rm(@backup_tar) end - it 'should set correct permissions on the tar file' do - expect(File.exist?(@backup_tar)).to be_truthy - expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + context 'archive file permissions' do + it 'should set correct permissions on the tar file' do + expect(File.exist?(@backup_tar)).to be_truthy + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + end + + context 'with custom archive_permissions' do + before do + allow(Gitlab.config.backup).to receive(:archive_permissions).and_return(0651) + # We created a backup in a before(:all) so it got the default permissions. + # We now need to do some work to create a _new_ backup file using our stub. + FileUtils.rm(@backup_tar) + create_backup + end + + it 'uses the custom permissions' do + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100651') + end + end end it 'should set correct permissions on the tar contents' do @@ -110,12 +137,9 @@ describe 'gitlab:app namespace rake task' do before :all do @origin_cd = Dir.pwd - Rake::Task["gitlab:backup:db:create"].reenable - Rake::Task["gitlab:backup:repo:create"].reenable - Rake::Task["gitlab:backup:uploads:create"].reenable + reenable_backup_sub_tasks - # Record the existing backup tars so we don't touch them - existing_tars = tars_glob + FileUtils.rm tars_glob # Redirect STDOUT and run the rake task orig_stdout = $stdout @@ -124,7 +148,7 @@ describe 'gitlab:app namespace rake task' do run_rake_task('gitlab:backup:create') $stdout = orig_stdout - @backup_tar = (tars_glob - existing_tars).first + @backup_tar = tars_glob.first end after :all do |