Package: oarepo-enrollments
oarepo-enrollmentsOArepo Enrollment library provides a unified way for admin, curator or other users
to ask person to enroll in a "task". The task might be anything - for example,
assigning role to a member, to participate on editing a record, etc.The user being enrolled might not yet exist in Invenio user database. If s/he does not,
the enrollment is completed after the user registers.The enrollment might be "automatic" as well - that is, if the user already exists,
no intervention is required from him/her.Enrollments can be grouped into one parent - that is, if user enrolls into parent enrollment,
he will be automatically enrolled into the children enrollments.Table of ContentsInstallationUsageEnrolling userHandler implementationHandler registrationRevoking userListing enrollmentsAPIenrollEnrollmentHandlerEnrollmentREST APIEnrolling user via POSTList enrollmentsGetting enrollmentRevoking enrollmentSecurityOAREPO_ENROLLMENT_LIST_PERMISSION_FACTORYOAREPO_ENROLLMENT_LIST_PERMISSION_FILTEROAREPO_ENROLLMENT_RETRIEVE_PERMISSION_FACTORYOAREPO_ENROLLMENT_ENROLL_PERMISSION_FACTORYOAREPO_ENROLLMENT_REVOKE_PERMISSION_FACTORYCommand-line toolsListing enrollments on cmdlineRevoking enrollments on cmdlineCreating enrollments on cmdlineConfigurationTemplatesSignalsInstallationpipinstalloarepo-enrollmentsUsageEnrolling userTo enroll user, callfromoarepo_enrollmentsimportenrollenroll(enrollment_type='role',recipient='sample.user@test.com',subject='You have become a curator !',body="""Dear user,by clicking on the link below you will become a curator for this repository.{{ enrollment_url }}Congratulations!""",# extra params need by the enrollment handler, in this case the role to assign the user toextra_data=dict(role='curators'))Alternatively, the email subject and body might be omitted - in that case the caller must
notify the user being enrolled.On the background, this will:A unique enrollment id is generated and associated with recipent email, kwargs are json-serialized
and together with id written to the database.IfmodeisENROLL_SKIP_EMAIL: A check is made if a user with this email
address already exists. If so, calls the enrollment handler (see below) and returns.a. An email is created and sent. The subject and body are processed as jinja templates,
receiving enrollment url in context variables.
b. or caller is responsible for sending the email / text message / notification / etc.User receives the email / message / notification and clicks on the enrollment linkUser must log in or register via standard invenio or 3-rd party registrationAn expiration is checked. If expired or already used, user is redirected to appropriate failure page.Enrollment url view checks if enrollment handler returns True fromacceptance_requiredproperty.
If so, user is redirected to theaccept_urland when accepts the invitation, timestamp is stored
in the db and user is redirected back to the enrollment url.Enrollment url view calls theenrollmethod on enrollment handler with:current User of the logged-in userany extra kwargs passed to the enroll callEnrollment handler performs enrollment action (whatever it is). On error raises EnrollmentExceptionThe database record is enriched with timestamp, enrollment status and user instance.user is redirected via 302 to the redirection success or failure url. These are taken from:urls passed to enroll functionurls retrieved from enrollment handlerdefault urls from flask configurationNote:once the enrollment link has been consumed, it can not be reused by a different user.Note:expired link can not be used to enroll, it might be used if user has already enrolled
and user will be redirected to the success url.Handler implementationEnrollment handler is a function with signature:fromoarepo_enrollmentsimportEnrollmentHandlerfrominvenio_accounts.modelsimportUser,Rolefrominvenio_dbimportdbclassAssignRole(EnrollmentHandler):defenroll(self,user:User,role=None,**kwargs)->bool:role=Role.query.filter_by(name=role).one()user.roles.append(role)db.session.add(user)db.session.commit()defrevoke(self,user:User,role=None,**kwargs)->bool:role=Role.query.filter_by(name=role).one()user.roles.remove(role)db.session.add(user)db.session.commit()Handler registrationRegister handler in setup.py:setup(# ...entry_points={'oarepo_enrollments.enrollments':['role = my.module:AssignRole',],})Revoking userfromoarepo_enrollmentsimportrevokerevoke(enrollment=<instanceofenrollment,keyorid>)Listing enrollmentsIf you have specified "external_key" when creating the enrollment, you can list the enrollments
by the key and enrollment type:fromoarepo_enrollmentsimportlist_enrollmentsforenrollmentinlist_enrollments(external_key='test',enrollment_type='role'):print(enrollment)APIenrollfromoarepo_enrollmentsimportenroll,ENROLL_MANUALLY,ENROLL_AUTOMATICALLY,ENROLL_SKIP_EMAILdefenroll(enrollment:str,recipient:str,sender:invenio_accounts.models.User,# optional, current_user is used if not specifiedsender_email:str,# optional, sender's email is used if not specifiedsubject:str,# jinja templatebody:str,# jinja templatehtml:bool,# set true if the body is a html documentlanguage:str,# language for flask-babelexmode:ENROLL_XXX,# see belowaccept_url:str,# override default accept urlreject_url:str,# override default reject urlsuccess_url='url',# override the success urlfailure_url='url',# override the failure urlcommit=True,# commit the changesexternal_key=None,# set an external key (any string)actions=None,# actionsparent_enrollment=None,# parent enrollment**kwargs# any kwargs)->None:passThemodeparameter can be:ENROLL_MANUALLY- always send the enrollment email and enroll user only after clicked on the linkENROLL_AUTOMATICALLY- if a user with the given email address exists, enroll him/her but send the mail
as wellENROLL_SKIP_EMAIL- if a user with the given email address exists, enroll him/her and skip the mailsubjectandbodyare jinja templates. The following variables are passed in:enrollment_url- full enrollment url**kwargs- all the kwargsuser- if the user has already registered, an instance ofinvenio_accounts.models.Userlanguage- language parameterurlsif passed override the default urls returned by the handler. The default implementation
of the handler returns urls from the configurationActionsmight be an array of strings associated with the enrollment. They can be used
to provide searchable granularity to enrollments. For example, if the enrollment means
"right to a collection", the actions might be an array of "read", "update", "delete".parent_enrollmentis an enrollment that will control the state of this one.
If it is specified, no email is sent as it is supposed that one has been already
sent for the parent enrollment. If the state of the parent enrollment changes, the
state of this one is changed accordingly.EnrollmentHandlerfromoarepo_enrollments.modelsimportEnrollmentfrominvenio_accounts.modelsimportUserclassEnrollmentHandler:def__init__(self,enrollment:Enrollment):self.enrollment=enrollmentdefenroll(self,user:User,**kwargs)->None:raiseNotImplementedError('Implement this')defrevoke(self,user:User,**kwargs)->None:raiseNotImplementedError('Implement this')acceptance_required=Falsetitle="human readable title, implicitly self.__doc__"email_template={'subject':None,'body':None,'html':None}# templates, might be overriden to have per-handler specific templatesuccess_template='oarepo/enrollment/success.html'failure_template='oarepo/enrollment/failure.html'accept_template='oarepo/enrollment/accept.html'reject_template='oarepo/enrollment/reject.html'# urls, might be overriden if for example using only rest APIenrollment_url="url on which user can enroll"accept_url="url on which user can accept the enrollment"reject_url="url to which user is redirected to when he rejected the enrollment"success_url="url to which user is redirected to when he accepted the enrollment"failure_url="url to which user is redirected to when enrollment failed"EnrollmentA database model containing enrollment status.REST APIEnrolling user via POSTPOST /api/enroll/{
    enrollment_type: "role", // enrollment type, role is a built in enrollment type
    recipient: "someone@example.com",
    email_template: "role-enrollment-email",
    language: "language for email translations",
    mode: "manual | automatic | skip_email",
    external_key: "caller key - any - for later identification",
    // any args that will get passed to the handler
    role: 'test',
    actions: ['test']
}Returns:{
    'id': 1,
    'enrollment_type': 'role',
    'enrolled_email': 'someone@example.com',
    'enrolled_user': null,
    'granting_user': {
        'email': 'granting@example.com'   // always the current user
    },
    'state': 'Pending',
    'external_key': 'caller key - any - for later identification',
    'extra_data': {
        'role': 'test'
    },
    'start_timestamp': '2020-12-04T08:48:12.873987',
    'expiration_timestamp': '2020-12-18T08:48:12.873987',
    'accepted_timestamp': null,
    'failure_reason': null,
    'finalization_timestamp': null,
    'rejected_timestamp': null,
    'revocation_timestamp': null,
    'revoker': null,
    'user_attached_timestamp': null,
    'actions': ['test']
}An email will be sent to the user with the content (given by the template, seeOAREPO_ENROLLMENT_MAIL_TEMPLATESin configuration):Content-Type:text/plain;charset="utf-8"MIME-Version:1.0Content-Transfer-Encoding:7bitSubject:NewroleassignedtoyouFrom:granting@example.comTo:someone@example.comDate:Fri,04Dec2020 10:11:08 +0100Message-ID:<160707306860.2310753.11343254980107038800@krokd.local>Reply-To:granting@example.commiroslav.simek@vscht.czwantstoassignyouarole"test"inUCTrepository.Toacceptorrejecttherole,pleaseclickonhttps://localhost/enroll/j2eh-ctr0-q2d6-4drb-y7kc-mapt-axfd-3053Thankyouforyourcooperation,repository@UCTNote:Key or enrollment url (sent via email) is not returned for security reasons. If you need to pass it
to javascript, make your own enrollment API. Seeoarepo_enrollment/views/api.pyfor details.List enrollmentsGET /api/enroll/?enrollment_type=<abc>&external_key=<abc>&state=[pending,linked,accepted,rejected,success,failure,revoked]&page=&size=10Returns:{
    "pagination": {
        "currentPage": "/api/enroll/?size=20&page=1",
        "hasNext": false,
        "hasPrev": false,
        "pages": 1,
        "size": 20,
        "totalElements": 1
    },
    "data": [
        {
            "id": 1,
            // as in get below
        }
    ]
}Note:Key (sent via email) is not returned for security reasons.Getting enrollmentGET /api/enroll/<id>Returns:{
    "id": 1,
    "enrollment_type": "role",
    "external_key": null,
    "enrolled_email": "someone@example.com",
    "enrolled_user": null,
    "granting_user": {
        "email": "granting@example.com"
    },
    "revoker": null,
    "extra_data": {
        "role": "test"
    },
    "state": "Pending",
    "actions": ["test"],
    "start_timestamp": "Thu, 03 Dec 2020 21:26:40 -0000",
    "expiration_timestamp": "Thu, 17 Dec 2020 21:26:40 -0000",
    "user_attached_timestamp": null,
    "accepted_timestamp": null,
    "rejected_timestamp": null,
    "finalization_timestamp": null,
    "revocation_timestamp": null,
    "failure_reason": null
}Note:Key (sent via email) is not returned for security reasons.Revoking enrollmentDELETE /api/enroll/<id>Returns:{
    'id': 1,
    'state': 'Revoked',
    'revocation_timestamp': '2020-12-04T08:48:12.873987',
    'revoker': {'email': 'revoker@example.com'},
    // ... rest of data
}Accepting enrollmentPOST /api/enroll/accept/<key>Returns:{
    status: 'ok',
    url: '<success url>'
}or{
    status: 'error',
    message: '<error message>'
}SecurityThe following configuration options define who has access to enrollments:OAREPO_ENROLLMENT_LIST_PERMISSION_FACTORYFactory (or import string) returning Permission (or an object withcanmethod) that limits access to listing.
For extensibility reasons the factory function must accept**kwargsOAREPO_ENROLLMENT_LIST_PERMISSION_FILTERA function (or import string) that takesEnrollment.queryas argument and returns filtered
query set. The function might, for example, limit the enrollments only to those created
by thecurrent_user.The default implementation returns the input query without modificationOAREPO_ENROLLMENT_RETRIEVE_PERMISSION_FACTORYFactory (or import string) that takesenrollment: Enrollmentinstance and returns Permission.
For extensibility reasons the factory function must accept**kwargsOAREPO_ENROLLMENT_ENROLL_PERMISSION_FACTORYFactory (or import string) that returns Permission representing if user can create an enrollment.
The factory gets enrollment data passed in request asenrollmentnamed parameter.
For extensibility reasons the factory function must accept**kwargsOAREPO_ENROLLMENT_REVOKE_PERMISSION_FACTORYFactory (or import string) that returns Permission representing if user can revoke an enrollment.
The factory gets enrollment instance asenrollment: Enrollmentnamed parameter.
For extensibility reasons the factory function must accept**kwargsCommand-line toolsListing enrollments on cmdline$inveniooarepo:enrolllist--state=Revoked

enrolled_useridkeyoperationrecipientstatetype-----------------------------------------------------------------------------2vschtread,update,delete,createsimeki@vscht.czRevokedrole

$inveniooarepo:enrolllist--state=Revoked--format=json[{"id":2,"type":"role","key":"vscht","recipient":"simeki@vscht.cz","enrolled_user":"","state":"Revoked","operation":"read,update,delete,create","actions":[]}]Options:--enrollment-type- filter for this enrollment type--external-key- only return enrollments with this external key--state- only return enrollments in this state. Can be a list of
"Pending", "Success", "Accepted", "Not accepted", "User attached", "Failed", "Revoked"
separated by comma--actions- only return enrollments with these comma-separated actionsRevoking enrollments on cmdline$inveniooarepo:enrollrevoke<enrollment_id>Creating enrollments on cmdline# invenio oarepo:enroll enroll <enrollment_type> <recipient_email> <external_key> <extra_data>$inveniooarepo:enrollenrollrolesimeki@vscht.czvschtrole=testArguments:enrollment_type- the enrollment type for the new enrollmentrecipient_email- the email of the recipinetexternal_key- any string identifying the enrollment for out-of-this-library purposesextra_data- any extra data required by the enrollment's handler. Either use:key=value{json_serialization of extra_data object}Options:--email-template- the email template to be used. If unset, defaults to 'default'--enrollment-method- the enrollment method. Use 'automatic', 'manual', 'skip-email',
see above for the meaning--expiration- override the default enrollment expiration periodConfiguration# default mail link expiration is 14 daysOAREPO_ENROLLMENT_EXPIRATION=14# url for redirectionOAREPO_ENROLLMENT_URL='/enroll/<key>'# redirection url if the link has expiredOAREPO_ENROLLMENT_EXPIRED_URL='/enroll/expired/<key>'# redirection url if the link has been already consumedOAREPO_ENROLLMENT_CONSUMED_URL='/enroll/consumed/<key>'# default accept urlOAREPO_ENROLLMENT_DEFAULT_ACCEPT_URL='/enroll/accept/<key>'# url that user is redirected to if he rejects an enrollmentOAREPO_ENROLLMENT_DEFAULT_REJECT_URL='/enroll/reject/<key>'# default url on success (if not specified by the task or caller)OAREPO_ENROLLMENT_DEFAULT_SUCCESS_URL='/enroll/success/<key>'# default url on failure (if not specified by the task or caller)OAREPO_ENROLLMENT_DEFAULT_FAILURE_URL='/enroll/failure/<key>'# if set, the Sender header will be added, From will be the enrolling userOAREPO_ENROLLMENT_REAL_SENDER_EMAIL=None# login url to redirect to when user is not logged in. If not set, SECURITY_LOGIN_URL is usedOAREPO_ENROLLMENT_LOGIN_URL=None# parameter name for login page that means "redirect after login is successfulOAREPO_ENROLLMENT_LOGIN_URL_NEXT_PARAM='next'# name of the base template, from which enrollment templates inherit. It must supply# title and content blocks.OAREPO_ENROLLMENT_BASE_TEMPLATE='oarepo/enrollment/base.html'# pre-configured mail templatesOAREPO_ENROLLMENT_MAIL_TEMPLATES={'default':{'subject':'You are being enrolled!','body':'Click {{ enrollment_url }} to participate.','html':False}}# user under which commandline tasks (enroll, revoke) are loggedOAREPO_ENROLLMENT_CMDLINE_USER=NoneTemplatesIf you use HTML views provided by this library, you can customize them as follows:Specify your own base template (path withintemplates) inOAREPO_ENROLLMENT_BASE_TEMPLATE.
The template must providetitleandcontentblocksOverride the templates completely in your application templates.
They are inoarepo/enrollmentfolder and are calledaccept.html,failure.html,reject.html,success.html. See the sources for passed
parameters etc.Override templates for a single enrollment type. In your handler, set the appropriate*_urlpropertiesSignalsimportoarepo_enrollments.signalsoarepo_enrollments.signals.enrollment_linked"""Notifies receivers that enrollment has been linked to an invenio user:param  enrollment: the linked enrollment"""oarepo_enrollments.signals.enrollment_created"""Notifies receivers that enrollment has been created.:param  enrollment: the linked enrollment:return True if no notification should be sent"""oarepo_enrollments.signals.enrollment_accepted"""Notifies receivers that enrollment has been accepted (when accept is required):param  enrollment: the accepted enrollment"""oarepo_enrollments.signals.enrollment_rejected"""Notifies receivers that enrollment has been rejected (when accept is required):param  enrollment: the rejected enrollment"""oarepo_enrollments.signals.enrollment_successful"""Notifies receivers that enrollment has been successfully carried out:param  enrollment: the successful enrollment"""oarepo_enrollments.signals.enrollment_failed"""Notifies receivers that enrollment failed.:param  enrollment: the failed enrollment:param  exception: failure exception"""oarepo_enrollments.signals.enrollment_duplicit_user"""Notifies receivers that the same enrollment is used by two different accounts.:param  enrollment: the failed enrollment:param  impostor: the second user that wants to use the enrollment"""oarepo_enrollments.signals.enrollment_revoked"""Notifies receivers that enrollment has been successfully revoked:param  enrollment: the revoked enrollment"""oarepo_enrollments.signals.revocation_failed"""Notifies receivers that revocation failed.:param  enrollment: the failed enrollment:param  exception: failure exception"""