Allow users to opt-in for automatic subscription to shares

Description

Evaluate different approaches to implement the feature of automatically subscribing external users to a share.

On share form any user can invite watchers:

We will send email invite to users.
We create account if email is not in DB.


We need to track all invites.
Email with link that render share and automatically subscribes to ticket.
We track if user accepted invitation.

We only send 1 invite to email.
If there is no subscribe_auto record - we send invitation email.
We create subscribe_auto record with NO_DECISION.
If there is subscribe_auto record with NO_DECISION / REJECT - we do not send invitation email.


we do not send more invitations via email from atlassian_host.
We do not create more subscribe_invitation for that user.

This will ensure our service wont be used as spam tool.
If Jira instance has more than 50 un-accepted invitations in last 30 days, we do not send more invitations via email.
User can see on dashboard new tab with invitations. Accepted, rejected, without-decision.

subscribe_invitation

id

bigint

host_id

int

share_id

bigint

invited_user_id

bigint

user to whom we send invite

sender_user_id

bigint

user who did send invite

atlassian_sender_user_id

status

enum {ACCEPTED, NO_DECISION}

by default NO_DECISION

create_date

date-time

action_date

date-time

date when ACCEPT

subscribe_auto

id

bigint

host_id

int

invited_user_id

bigint

user to whom we send invite

status

enum {ACCEPTED, NO_DECISION, REJECTED}

by default NO_DECISION

create_date

date-time

If user rejects all watch invitation - he can get another invitations notifications.
We base our spam protection based on NO_DECISION.


What happens during the invite action:

  • New record inserted to subscribe_auto (if no record)
    by default NO_DECISION

  • New record inserted to subscribe_invitation,
    status NO_DECISION - if subscribe_auto is NO_DECISION or REJECTED
    status ACCEPTED - if subscribe_auto is ACCEPTED

Example invite email

Subject: You are invited to subscribe to {SHARE_NAME}
Hello,

You can subscribe to {SHARE_NAME} by clicking:
https://jira.external-share.com/subscribe/{id-hash}


Thanks,
Externals Share for Jira Team


Link:
https://jira.external-share.com/subscribe/{id}/{hash}

{id} - invitation id
{hash} - hash(invitation id + share.secret_key)

Action - the link is clicked, then:

we change the status in subscribe_auto to ACCEPTED

status in subscribe_invitation to ACCEPTED.

If the user has an unconfirmed account (i.e. registration status is initial or pending),

then we redirect to the registration completion page https://jira.external-share.com/activate.html?code={code} .

If user has confirmed account, redirect to share /issue/{share-uuid}



Dashboard - 2 new tabs/tables.
1. Subscribe invitations

Share name

Invited by

Invited from

Example Roadmap

John

example.atlassian.net

Example Roadmap 2

Emilli

example2.atlassian.net

In case Invited by is atlassian_sender_user_id then:
we retrieve the name asynchronously.
In the table we insert <div atlassian-acconut-id='12345'>12345</div>.
In html header:

<meta name="atlassian-acconut-id-fetch" content="{signed-url-to-fetch-all-account-ids}">

signed-url-to-fetch-all-account-ids= /atlassian-user/{jwt}
jwt → payload - list of atlassian-acconut-id - signed. Example - status change on share ticket.

2. Subscribe permissions -
A list of hosts from which we allow an external user to be added to the subscription automatically (without confirmation). With the ability to undo this.

Jira name

Status

Action

example.atlassian.net

Allowed

<Block>

example2.atlassian.net

Blocked

<Allow>

If user is logged to account A and gets invite for account B, we should show error page with message like: “Subscription invitation is for another account”. Subscription invite should not be accepted.

Additional notes:

  • Pagination should be done on Subscribe invitations and Subscribe permissions tabs.

  • In Subscribe invitations and Subscribe permissions tables status should look like a status in subscriptions tab.

  • Invitation date should be added to Subscribe invitations table.

Linked issues

blocks
Issue Type Icon ESFJ-523 Default subscription of all shared issues for share participants Priority: Medium
Backlog

Activity

Mykhailo Iereshchenko 25 April 2025, 14:13

All information about task can be found in description and comments. In case of any questions feel free to contact me.

Automation for Jira 25 April 2025, 14:12

Hello @Mykhailo Iereshchenko

This is the best moment to add more information that can be helpful for tester.

  • What areas are affected?

  • What are potential edge cases?

  • Was it checked for XSS problems?

  • Does change affect security, is new data exposed?

Please attach - Before / After screenshot if possible.

Krzysztof Bogdan 25 April 2025, 12:26

@Mykhailo Iereshchenko There is 1 non-blocking issues

Automation for Jira 25 April 2025, 12:26

Hello @Mykhailo Iereshchenko,
Change was reviewed and approved.
Task is ready to be deployed to QA.
Once it is deployed to QA please move ticket to "To Test"

Thank you!

Krzysztof Bogdan 25 April 2025, 11:21

@Mykhailo Iereshchenko Yea. 1 hour sounds good

Mykhailo Iereshchenko 25 April 2025, 10:15

@Krzysztof Bogdan There is a limitation of 50 unaccepted invitations send from one instance in past 30 days. You asked to reduce number of unaccepted invitations from 50 to 5 on qa and local environments. Should I also reduce number of days? If yes, to what value? 1h would be ok?

Krzysztof Bogdan 25 April 2025, 07:37

@Mykhailo Iereshchenko Why does it matter. It is about relation with host_id. not user id?

Mykhailo Iereshchenko 24 April 2025, 20:31

@Krzysztof Bogdan In DB we have two columns which identify user who sent the invitation: atlassian_sender_user_id and app_sender_user_id. In LOCAL/QA tab we show for example “Number of sent and accepted by invited users invitations”. So this should be a number of invitations which where send by atlassian_sender_user_id (i.e. from preview)? Because we cannot detect app_sender_user_id being in jira

Mykhailo Iereshchenko 15 April 2025, 07:46

In additional feature requested by @Krzysztof Bogdan there should be:

  • New tab which will be available only in local and qa environments

  • Number of sent and accepted by invited users invitations

  • Number of sent and unaccepted by invited users invitations, which should be set to 5 for local and qa environment and 50 for production

  • Status blocked if number if unaccepted invites reaches the limit

  • Reset button which deletes all unaccepted invites

Krzysztof Bogdan 26 March 2025, 16:13

@Mykhailo Iereshchenko I know that would be simpler.

But… we can not do that.

If list have 100 rows.
That would mean 100 HTTP CALLS to Atlassian.

That would mean loading list will be very, very slow.

If we can we should load related data in async manner without blocking page load.

Also we should gather all user ids and fetch users in 1 http call.

// backend example: private UsersInGroupListResponse getUsers(List<String> accountIds,                                              int startAt) {        var accountId = String.join("&accountId=", accountIds.stream()            .map(UriQueryBuilder::encodeUriComponent)            .toList());        var path = "/rest/api/3/user/bulk?accountId=" + accountId +                   "&startAt=" + startAt +                   "&maxResults=" + 200;        return httpClient.get(path, UsersInGroupListResponse.class);    }// frontend example:    bulkGetUsers(opts) {        return new Promise((resolve, error) => this.req({            url: "/rest/api/3/user/bulk",            method: "GET",            query: opts,            response: 'json',            success: function (result) {                resolve(result);            },            error: function (result) {                error(result);            }        }));    }
Mykhailo Iereshchenko 26 March 2025, 13:26

@Krzysztof Bogdan

Should this part be done the exact same way you described? Because I could just make a call to “/rest/api/3/user?accountId=” and retrieve the name because that's all we need to display in the table. It seems to me that this would be simpler. Below is the function that retrieves all invitations for a given user. atlassianSenderUserIdis used to retrieve a name of the user which sent the invite.

Mykhailo Iereshchenko 26 March 2025, 12:22

@Krzysztof Bogdan If our app_user is currently logged into account A, gets an invitation to account B and clicks on the link in the email, should he be logged out of the current account (A) and be redirected to /login.html? If so, should the invitation be accepted as soon as user clicks the link, or only after logging in?
Or maybe, it would be better not to log in and just display some page with a message like “You've successfully accepted watcher invitation”?
Currently, when I just redirect to /login.html it automatically logs into the account I'm currently logged (A).