#308 GDPR compliance
Merged 5 years ago by msuchy. Opened 5 years ago by frostyx.

file modified
+3 -1
@@ -16,7 +16,7 @@ 

  %staticdir/copr_logo.png                        \

  %staticdir/css/style-overwrite.css              \

  %templatedir/project_info.html                  \

- %templatedir/user_info.html                     \

+ %templatedir/user_meta.html                     \

  %templatedir/welcome.html

  

  %global devel_files \
@@ -87,6 +87,7 @@ 

  BuildRequires: python3-pygments

  BuildRequires: python3-flask-whooshee

  BuildRequires: python3-modulemd

+ BuildRequires: python3-simplejson

  BuildRequires: redis

  %endif

  
@@ -122,6 +123,7 @@ 

  Requires: python3-CommonMark

  Requires: python3-psycopg2

  Requires: python3-zmq

+ Requires: python3-simplejson

  Requires: xstatic-patternfly-common

  Requires: js-jquery1

  Requires: xstatic-jquery-ui-common

@@ -65,6 +65,8 @@ 

  from coprs.views.tmp_ns import tmp_general

  from coprs.views.groups_ns import groups_ns

  from coprs.views.groups_ns import groups_general

+ from coprs.views.user_ns import user_ns

+ from coprs.views.user_ns import user_general

  from coprs.views.webhooks_ns import webhooks_ns

  from coprs.views.webhooks_ns import webhooks_general

  
@@ -83,6 +85,7 @@ 

  app.register_blueprint(stats_receiver.stats_rcv_ns)

  app.register_blueprint(tmp_ns.tmp_ns)

  app.register_blueprint(groups_ns)

+ app.register_blueprint(user_ns)

  app.register_blueprint(webhooks_ns)

  

  app.add_url_rule("/", "coprs_ns.coprs_show", coprs_general.coprs_show)

@@ -1,7 +1,12 @@ 

+ import json

+ import simplejson

  from coprs import exceptions

+ from flask import url_for

  

  from coprs import app, db

  from coprs.models import User, Group

+ from coprs.helpers import copr_url

+ from sqlalchemy import update

  

  

  class UsersLogic(object):
@@ -102,3 +107,69 @@ 

              return fas_group in app.config["BLACKLISTED_GROUPS"]

          else:

              return False

+ 

+     @classmethod

+     def delete_user_data(cls, fas_name):

+         query = update(User).where(User.username==fas_name).\

+             values(

+                 timezone=None,

+                 proven=False,

+                 admin=False,

+                 proxy=False,

+                 api_login='',

+                 api_token='',

+                 api_token_expiration='1970-01-01',

+                 openid_groups=None

+             )

+         db.engine.connect().execute(query)

+ 

+ 

+ class UserDataDumper(object):

+     def __init__(self, user):

+         self.user = user

+ 

+     def dumps(self, pretty=False):

+         if pretty:

+             return simplejson.dumps(self.data, indent=2)

+         return json.dumps(self.data)

+ 

+     @property

+     def data(self):

+         data = self.user_information

+         data["groups"] = self.groups

+         data["projects"] = self.projects

+         data["builds"] = self.builds

+         return data

+ 

+     @property

+     def user_information(self):

+         return {

+             "username": self.user.name,

+             "email": self.user.mail,

+             "timezone": self.user.timezone,

+             "api_login": self.user.api_login,

+             "api_token": self.user.api_token,

+             "api_token_expiration": self.user.api_token_expiration.strftime("%b %d %Y %H:%M:%S"),

+             "gravatar": self.user.gravatar_url,

+         }

+ 

+     @property

+     def groups(self):

+         return [{"name": g.name,

+                  "url": url_for("groups_ns.list_projects_by_group", group_name=g.name, _external=True)}

+                 for g in self.user.user_groups]

+ 

+     @property

+     def projects(self):

+         # @FIXME We get into circular import when this import is on module-level

+         from coprs.logic.coprs_logic import CoprsLogic

+         return [{"full_name": p.full_name,

+                  "url": copr_url("coprs_ns.copr_detail", p, _external=True)}

+                 for p in CoprsLogic.filter_by_user_name(CoprsLogic.get_multiple(), self.user.name)]

+ 

+     @property

+     def builds(self):

+         return [{"id": b.id,

+                  "project": b.copr.full_name,

+                  "url": copr_url("coprs_ns.copr_build", b.copr, build_id=b.id, _external=True)}

+                 for b in self.user.builds]

@@ -7,6 +7,7 @@ 

  {% block body %}

  <div class="row">

    <div class="col-md-9 col-sm-8">

+   {% block content %}

      {% block show_top %}

      {% endblock %}

      <div class="panel panel-default">
@@ -39,6 +40,7 @@ 

        </div>

      </div>

      {{ render_pagination(request, paginator) }}

+   {% endblock %}

    </div>

    <div class="col-md-3 col-sm-4">

      <br>

@@ -29,7 +29,12 @@ 

          {% endif %}

        </a>

  

-       {% include "user_info.html" %}

+       {% if g.user %}

+       | <a href="{{ url_for('user_ns.user_info') }}">User Info</a>

+       {% endif %}

+ 

+ 

+       {% include "user_meta.html" %}

    </p>

  </div>

  

@@ -115,6 +115,7 @@ 

                        <li> <a href="{{ url_for('coprs_ns.coprs_show') }}">Home</a> </li>

                        <li> <a href="{{url_for('status_ns.importing')}}">Task Queue</a> </li>

                        <li> <a href="{{ url_for('api_ns.api_home') }}">API</a> </li>

+                       <li><a href="{{ url_for('user_ns.user_info') }}">GDPR</a></li>

                      </ul>

                    </dd>

                  </dl>

@@ -1,4 +1,50 @@ 

- |

- <a href="https://badges.fedoraproject.org/user/{{ user.name }}" title="{{ user.name }}'s badges" target="_blank">

-     Fedora badges

+ {% extends "coprs/show.html" %}

+ {% block title %}User Info{% endblock %}

+ {% block header %}User Info{% endblock %}

+ {% block breadcrumbs %}

+ <ol class="breadcrumb">

+     <li>

+         <a href="{{ url_for('coprs_ns.coprs_show') }}">Home</a>

+     </li>

+     <li>

+         <a href="{{ url_for('coprs_ns.coprs_by_user', username=user.name) }}">{{ user.name }}</a>

+     </li>

+     <li class="active">

+         User Info

+     </li>

+ </ol>

+ {% endblock %}

+ 

+ {% block content %}

+ <h1>User Info</h1>

+ 

+ <p>

+     According to the GDPR (General Data Protection Regulation 2016/679 of the European Union), users have right to see

+     all the data, that service stores about them, and also right to delete them. Read more about your

+     <a href="https://fedoraproject.org/wiki/Legal:PrivacyPolicy#Your_Rights_and_Choices_in_the_EEA">rights and privacy in the Fedora Project</a>.

+ </p>

+ 

+ 

+ <h2>Obtaining data</h2>

+ <p>

+     It is possible to download a JSON file with a dump of all the information that Copr knows about you (e.g. list of

+     your projects, your builds, contact information, API tokens, etc).

+ </p>

+ <a href="{{ url_for('user_ns.user_info_download') }}">

+     <button type="button" class="btn btn-primary">Download my data</button>

  </a>

+ 

+ 

+ <h2>Delete data</h2>

+ <p>

+     This will delete the information Copr knows about you, such as timezone and API tokens. However, your username and

+     email cannot be deleted as they are tied to the FAS account. Moreover, if you wish to delete your builds/projects,

+     you have to do so manually (possibly using the data acquired above).

+ </p>

+ <p>

+     You can still log in at any later time. However, you will have to create a new API token.

+ </p>

+ <a href="{{ url_for('user_ns.delete_data') }}" onclick="return confirm('Are you sure you want to delete your data?');">

+     <button type="button" class="btn btn-danger" action="">Delete my data</button>

+ </a>

+ {% endblock %}

@@ -0,0 +1,4 @@ 

+ |

+ <a href="https://badges.fedoraproject.org/user/{{ user.name }}" title="{{ user.name }}'s badges" target="_blank">

+     Fedora badges

+ </a>

@@ -33,7 +33,6 @@ 

  from coprs.logic.coprs_logic import CoprsLogic

  from coprs.logic.packages_logic import PackagesLogic

  from coprs.logic.stat_logic import CounterStatLogic

- from coprs.logic.users_logic import UsersLogic

  from coprs.logic.modules_logic import ModulesLogic, ModulemdGenerator, ModuleBuildFacade

  from coprs.rmodels import TimedStatEvents

  

@@ -0,0 +1,4 @@ 

+ import flask

+ 

+ 

+ user_ns = flask.Blueprint("user_ns", __name__, url_prefix="/user")

@@ -0,0 +1,39 @@ 

+ import flask

+ from . import user_ns

+ from coprs.views.misc import login_required

+ from coprs.logic.users_logic import UsersLogic, UserDataDumper

+ from coprs.logic.builds_logic import BuildsLogic

+ from coprs.logic.complex_logic import ComplexLogic

+ 

+ 

+ def render_user_info(user):

+     graph = BuildsLogic.get_running_tasks_from_last_day()

+     return flask.render_template("user_info.html",

+                                  user=user,

+                                  tasks_info=ComplexLogic.get_queue_sizes(),

+                                  graph=graph)

+ 

+ 

+ @user_ns.route("/info")

+ @login_required

+ def user_info():

+     return render_user_info(flask.g.user)

+ 

+ 

+ @user_ns.route("/info/download")

+ @login_required

+ def user_info_download():

+     user = flask.g.user

+     dumper = UserDataDumper(user)

+     response = flask.make_response(dumper.dumps(pretty=True))

+     response.mimetype = "application/json"

+     response.headers["Content-Disposition"] = "attachment; filename={0}.json".format(user.name)

+     return response

+ 

+ 

+ @user_ns.route("/delete")

+ @login_required

+ def delete_data():

+     UsersLogic.delete_user_data(flask.g.user.username)

+     flask.flash("Your data were successfully deleted.")

I'd probably request "are you sure" confirmation.

+     return render_user_info(flask.g.user)

@@ -15,7 +15,7 @@ 

  from coprs import db

  from coprs import exceptions

  from coprs import models

- from coprs.logic import coprs_logic, packages_logic, actions_logic, builds_logic

+ from coprs.logic import coprs_logic, packages_logic, actions_logic, builds_logic, users_logic

  from coprs.views.misc import create_user_wrapper

  from coprs.whoosheers import CoprWhoosheer

  from sqlalchemy import or_
@@ -332,6 +332,21 @@ 

      )

  

  

+ class DumpUserCommand(Command):

+ 

+     def run(self, username):

+         user = models.User.query.filter(models.User.username == username).first()

+         if not user:

+             print("There is no user named {0}.".format(username))

+             return 1

+         dumper = users_logic.UserDataDumper(user)

+         print(dumper.dumps(pretty=True))

+ 

+     option_list = (

+         Option("username"),

+     )

+ 

+ 

  class AlterUserCommand(Command):

  

      def run(self, name, **kwargs):
@@ -457,6 +472,7 @@ 

  manager.add_command("drop_chroot", DropChrootCommand())

  manager.add_command("alter_user", AlterUserCommand())

  manager.add_command("add_user", AddUserCommand())

+ manager.add_command("dump_user", DumpUserCommand())

  manager.add_command("fail_build", FailBuildCommand())

  manager.add_command("update_indexes", UpdateIndexesCommand())

  manager.add_command("update_indexes_quick", UpdateIndexesQuickCommand())

@@ -0,0 +1,7 @@ 

+ #!/bin/bash

+ # Purpose of this script is to be remotely executed from batcave01

+ # to collect user SAR data

+ # Read more: http://fedora-infra-docs.readthedocs.io/en/latest/sysadmin-guide/sops/gdpr_sar.html

+ # Playbook: https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/playbooks/manual/gdpr/sar.yml

+ # Usage: SAR_USERNAME=someusername copr-gdpr-sar.sh

+ copr-frontend dump_user $SAR_USERNAME

@@ -0,0 +1,54 @@ 

+ import flask

+ import json

+ from coprs import app

+ from coprs.logic.users_logic import UsersLogic, UserDataDumper

+ from tests.coprs_test_case import CoprsTestCase

+ 

+ 

+ app.config["SERVER_NAME"] = "localhost"

+ 

+ 

+ class TestUserDataDumper(CoprsTestCase):

+ 

+     def test_user_information(self, f_users, f_fas_groups, f_coprs, f_db):

+         dumper = UserDataDumper(self.u1)

+         data = dumper.user_information

+         assert data["username"] == "user1"

+         assert data["email"] == "user1@foo.bar"

+         assert data["timezone"] is None

+         assert data["api_login"] == "abc"

+         assert data["api_token"] == "abc"

+         assert data["api_token_expiration"] == "Jan 01 2000 00:00:00"

+         assert data["gravatar"].startswith("https://seccdn.libravatar.org/avatar/")

+ 

+     def test_projects(self, f_users, f_coprs, f_db):

+         with app.app_context():

+             dumper = UserDataDumper(self.u1)

+             projects = dumper.projects

+             assert [p["full_name"] for p in projects] == ["user1/foocopr"]

+ 

+     def test_builds(self, f_users, f_coprs, f_builds, f_db):

+         with app.app_context():

+             dumper = UserDataDumper(self.u1)

+             builds = dumper.builds

+             assert len(builds) == 1

+             assert builds[0]["id"] == 1

+             assert builds[0]["project"] == "user1/foocopr"

+ 

+     def test_data(self, f_users, f_fas_groups, f_coprs, f_db):

+         with app.app_context():

+             dumper = UserDataDumper(self.u1)

+             data = dumper.data

+             assert "username" in data

+             assert type(data["groups"]) == list

+             assert type(data["projects"]) == list

+             assert type(data["builds"]) == list

+ 

+     def test_dumps(self, f_users, f_fas_groups, f_coprs, f_db):

+         with app.app_context():

+             dumper = UserDataDumper(self.u1)

+             output = dumper.dumps()

+             assert type(output) == str

+             data = json.loads(output)

+             assert "username" in data

+             assert "projects" in data

I and @dturecek have implemented user data retrieval and deletion, so Copr can be GDPR compatible.
Also, there [1] is a patch for infrastructure playbooks. It is needed because of [2].

[1] https://frostyx.fedorapeople.org/pagure/0001-Collect-GDPR-SAR-information-also-from-Copr.patch
[2] http://fedora-infra-docs.readthedocs.io/en/latest/sysadmin-guide/sops/gdpr_sar.html

Security seems to be fine, nobody can retrieve/delete data related to different user. +1

1 new commit added

  • [frontend] fix english mistakes
5 years ago

@praiskup, thank you for the review. I've fixed the English mistakes.

I'd probably request "are you sure" confirmation.

@dturecek, can you please look on it? I think that even javascript confirmation would be sufficient if you find it easier to implement.

1 new commit added

  • [frontend] add confirmation for delete user data action
5 years ago

I've added the confirmation. Do you think it's enough to do it this way?

I've added the confirmation. Do you think it's enough to do it this way?

Yes, that's what I meant. I think that it is good enough :-)

Pull-Request has been merged by msuchy

5 years ago