diff --git a/services/webapp/code/rosetta/auth.py b/services/webapp/code/rosetta/auth.py index 90b431b78bad20ac0bafb13dc5060f6dec02b409..207ce59c4cf226089700fc753ca67c7aabdff55e 100644 --- a/services/webapp/code/rosetta/auth.py +++ b/services/webapp/code/rosetta/auth.py @@ -14,7 +14,7 @@ class RosettaOIDCAuthenticationBackend(OIDCAuthenticationBackend): user = super(RosettaOIDCAuthenticationBackend, self).create_user(claims) # Add profile, keys etc. - finalize_user_creation(user) + finalize_user_creation(user, auth='oidc') return user diff --git a/services/webapp/code/rosetta/context_processors.py b/services/webapp/code/rosetta/context_processors.py index 67c443563eb3182827d34ecb957a21baa94eb596..0d563a37927392e1c51546642f05a5779c78ae24 100644 --- a/services/webapp/code/rosetta/context_processors.py +++ b/services/webapp/code/rosetta/context_processors.py @@ -8,7 +8,13 @@ def export_vars(request): data['OPENID_ENABLED'] = True else: data['OPENID_ENABLED'] = False - + + # Set local auth enabled or not + if settings.DISABLE_LOCAL_AUTH: + data['LOCAL_AUTH_ENABLED'] = False + else: + data['LOCAL_AUTH_ENABLED'] = True + # Set invitation code required or not if settings.INVITATION_CODE: data['INVITATION_CODE_ENABLED'] = True diff --git a/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py b/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py index 0b3d3ec80dcbeaae259a95b2889aa54aa9b56169..9ba4c7d501f15f681f3b328f7219206b15ce9aed 100644 --- a/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py +++ b/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py @@ -35,7 +35,7 @@ class Command(BaseCommand): testuser.is_superuser=True testuser.save() print('Creating testuser profile') - Profile.objects.create(user=testuser, authtoken='129aac94-284a-4476-953c-ffa4349b4a50') + Profile.objects.create(user=testuser, auth='local', authtoken='129aac94-284a-4476-953c-ffa4349b4a50') # Create default keys print('Creating testuser default keys') @@ -82,8 +82,8 @@ class Command(BaseCommand): <br/><br/> A test user with admin rights registered with email <code>testuser@rosetta.platform</code> and password <code>testpass</code> has been created as well, which you can use to login on the menu on the right and give Rosetta - immediately a try. If you run with the default docker-compose file (i.e. you just run <code>rosetta/setup</code>), - then you will also have a few demo computing and storage resources (beside the internal engine) already available + immediately a try. If you are using the default docker-compose file (i.e. you just ran <code>rosetta/setup</code>), + then you will also have a few demo computing and storage resources (beside the internal one) already available and that you can play with, including a small Slurm cluster. Otherwise, you will need to setup your own ones from the <a href="/admin">admin</a> section. <br /> diff --git a/services/webapp/code/rosetta/core_app/migrations/0020_profile_auth.py b/services/webapp/code/rosetta/core_app/migrations/0020_profile_auth.py new file mode 100644 index 0000000000000000000000000000000000000000..26844ef727145c21d79efe486a526a839720393c --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0020_profile_auth.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2021-11-15 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0019_auto_20211115_1547'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='auth', + field=models.CharField(default='oidc', max_length=36, verbose_name='User auth mode'), + preserve_default=False, + ), + ] diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py index 01685d41007675d516c2a10827d0ddf7f8383a0c..daed8e165b032b5132eb177aaa2ee3d955225115 100644 --- a/services/webapp/code/rosetta/core_app/models.py +++ b/services/webapp/code/rosetta/core_app/models.py @@ -43,8 +43,9 @@ class Profile(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.OneToOneField(User, on_delete=models.CASCADE) + auth = models.CharField('User auth mode', max_length=36) timezone = models.CharField('User Timezone', max_length=36, default='UTC') - authtoken = models.CharField('User auth token', max_length=36, blank=True, null=True) + authtoken = models.CharField('User auth token', max_length=36, blank=True, null=True) # This is used for testing, not a login token. is_power_user = models.BooleanField('Power user status', default=False) extra_confs = JSONField(blank=True, null=True) diff --git a/services/webapp/code/rosetta/core_app/templates/account.html b/services/webapp/code/rosetta/core_app/templates/account.html index 2ca2d5ab1cc38894e6763fd3a1c5944df689abcd..d5be00747fc5bbb694cd60bfcd162b48d0e78e93 100644 --- a/services/webapp/code/rosetta/core_app/templates/account.html +++ b/services/webapp/code/rosetta/core_app/templates/account.html @@ -19,7 +19,7 @@ <tr> <td> - <b>Account ID</b> + <b>ID</b> </td> <td> {{data.user.username}} @@ -28,7 +28,7 @@ <tr> <td> - <b>Account email</b> + <b>Email</b> </td> <td> {% if data.edit == 'email' %} @@ -39,14 +39,15 @@ <input type="submit" value="Go"> </td></tr></table> {% else %} - {{data.user.email}} | <a href="/account/?edit=email">Change</a> + {{data.user.email}}{% if data.user.profile.auth == 'local' %} | <a href="/account/?edit=email">Change</a>{% endif %} {% endif %} </td> </tr> + {% if data.user.profile.auth == 'local' %} <tr> <td> - <b>Account password</b> + <b>Password</b> </td> <td> {% if data.edit == 'password' %} @@ -60,7 +61,26 @@ ******* | <a href="/account/?edit=password">Change</a> {% endif %} </td> - </tr> + </tr> + {% endif %} + + + <tr> + <td> + <b>Auth</b> + </td> + <td> + {% if data.user.profile.auth == 'local' %} + Local + {% elif data.user.profile.auth == 'oidc' %} + Open ID Connect + {% else %} + {{ data.user.profile.auth }} + {% endif %} + </td> + </tr> + + </table> <br /> @@ -129,7 +149,7 @@ </form> <div style="margin-left:10px; margin-top:40px"> - {% if OPENID_ENABLED %} + {% if data.user.profile.auth == 'oidc' %} <form action="{% url 'oidc_logout' %}" method="post"> {% csrf_token %} <input type="submit" value="logout"> diff --git a/services/webapp/code/rosetta/core_app/templates/navigation.html b/services/webapp/code/rosetta/core_app/templates/navigation.html index 3004bdb474a2a98cf1a3fd8de831748e77c1c66f..ecc8fbf4b6300fbef23c43e54dcbe5dc576369aa 100644 --- a/services/webapp/code/rosetta/core_app/templates/navigation.html +++ b/services/webapp/code/rosetta/core_app/templates/navigation.html @@ -36,6 +36,9 @@ <a href="/account" onclick = $("#menu-close").click(); >Account</a> </li> {% else %} + + + {% if LOCAL_AUTH_ENABLED %} <li> <center> <form class="form-signin" role="form" action='/login/' method='POST'> @@ -44,19 +47,28 @@ <input type="password" class="form-control" placeholder="Password" name='password'> <input type='submit' class="btn btn-lg ha-btn-lg" value='Login' /> </form> - {% if OPENID %} - {% endif %} </center> </li> + + {% if OPENID_ENABLED %} + <center><div style="margin-top:15px;margin-bottom:10px"><font color="#a9a9a9"> — OR —</font></div></center> + {% endif %} + + {% endif %} <center> {% if OPENID_ENABLED %} - <div style="margin-top:15px;margin-bottom:10px"><font color="#a9a9a9"> — OR —</font></div> + {% if LOCAL_AUTH_ENABLED %} <li style="padding-left:0; text-indent: 0"> <a href="{% url 'oidc_authentication_init' %}" style="padding-left:0; text-indent: 0">Login with OpenID Connect</a></li> + {% else %} + <li style="padding-left:0; text-indent: 0"> <a href="{% url 'oidc_authentication_init' %}" style="padding-left:0; text-indent: 0">Login</a></li> {% endif %} + {% endif %} + {% if LOCAL_AUTH_ENABLED %} <div style="padding:10px;"> <font color="gray">Forgot password? Just leave it empty to get a login link by email. Or, <a href="/register" style="color: #c0c0c0" onclick = $("#menu-close").click(); >Register</a>.</font> </div> + {% endif %} </center> {% endif %} diff --git a/services/webapp/code/rosetta/core_app/utils.py b/services/webapp/code/rosetta/core_app/utils.py index 04c0e1f26a8ef7a7a8d32d1d189fbeb645095570..1818e42164b174a4af42c0c4504c1b2e5da87790 100644 --- a/services/webapp/code/rosetta/core_app/utils.py +++ b/services/webapp/code/rosetta/core_app/utils.py @@ -134,13 +134,21 @@ def random_username(): return username -def finalize_user_creation(user): +def finalize_user_creation(user, auth='local'): from .models import Profile, KeyPair + # Just an extra check + try: + Profile.objects.get(user=user) + except Profile.DoesNotExists: + pass + else: + raise Exception('Consistency error: already found a profile for user "{}"'.format(user)) + # Create profile logger.debug('Creating user profile for user "{}"'.format(user.email)) - Profile.objects.create(user=user) + Profile.objects.create(user=user, auth=auth) # Generate user keys out = os_shell('mkdir -p /data/resources/keys/', capture=True) diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py index 1904a01e646efc125756f96a8a1f07f0ce2b8a3c..80f454208e16173b060ec5e5af49b827e1fca4ef 100644 --- a/services/webapp/code/rosetta/core_app/views.py +++ b/services/webapp/code/rosetta/core_app/views.py @@ -58,6 +58,9 @@ def login_view(request): return render(request, 'success.html', {'data': data}) if password: + if user.profile.auth != 'local': + # This actually hides that the user cannot be authenticated using the local auth. + raise ErrorMessage('Check email and password') user = authenticate(username=username, password=password) if user: login(request, user) @@ -66,28 +69,30 @@ def login_view(request): raise ErrorMessage('Check email and password') else: - # If empty password, send mail with login token - logger.debug('Sending login token via mail to {}'.format(user.email)) - - token = uuid.uuid4() + # If empty password and local auth, send mail with login token + if user.profile.auth == 'local': - # Create token or update if existent (and never used) - try: - loginToken = LoginToken.objects.get(user=user) - except LoginToken.DoesNotExist: - LoginToken.objects.create(user=user, token=token) - else: - loginToken.token = token - loginToken.save() - try: - send_email(to=user.email, subject='Rosetta login link', text='Hello,\n\nhere is your login link: https://{}/login/?token={}\n\nOnce logged in, you can go to "My Account" and change password (or just keep using the login link feature).\n\nThe Rosetta Team.'.format(settings.ROSETTA_HOST, token)) - except Exception as e: - logger.error(format_exception(e)) - raise ErrorMessage('Something went wrong. Please retry later.') - - # Return here, we don't want to give any hints about existing users - data['success'] = 'Ok, if we have your data you will receive a login link by email shortly.' - return render(request, 'success.html', {'data': data}) + logger.debug('Sending login token via mail to {}'.format(user.email)) + + token = uuid.uuid4() + + # Create token or update if existent (and never used) + try: + loginToken = LoginToken.objects.get(user=user) + except LoginToken.DoesNotExist: + LoginToken.objects.create(user=user, token=token) + else: + loginToken.token = token + loginToken.save() + try: + send_email(to=user.email, subject='Rosetta login link', text='Hello,\n\nhere is your login link: https://{}/login/?token={}\n\nOnce logged in, you can go to "My Account" and change password (or just keep using the login link feature).\n\nThe Rosetta Team.'.format(settings.ROSETTA_HOST, token)) + except Exception as e: + logger.error(format_exception(e)) + raise ErrorMessage('Something went wrong. Please retry later.') + + # Return here, we don't want to give any hints about existing users + data['success'] = 'Ok, if we have your data you will receive a login link by email shortly.' + return render(request, 'success.html', {'data': data}) else: @@ -270,24 +275,20 @@ def account(request): # Email elif edit=='email' and value: + # If no local auth, you should never get here + if request.user.profile.auth != 'local': + raise ErrorMessage('Cannot change password using an external authentication service') request.user.email=value request.user.save() # Password elif edit=='password' and value: + # If no local auth, you should never get here + if request.user.profile.auth != 'local': + raise ErrorMessage('Cannot change password using an external authentication service') request.user.set_password(value) request.user.save() - # API key - elif edit=='apikey' and value: - profile.apikey=value - profile.save() - - # Plan - elif edit=='plan' and value: - profile.plan=value - profile.save() - # Generic property elif edit and value: raise Exception('Attribute to change is not valid') diff --git a/services/webapp/code/rosetta/settings.py b/services/webapp/code/rosetta/settings.py index 464cd04bf0a249566eb91ed0d0ab6afcfd3b11d8..b774e15ea537bbb02ac47680b48763ec232da9db 100644 --- a/services/webapp/code/rosetta/settings.py +++ b/services/webapp/code/rosetta/settings.py @@ -235,7 +235,9 @@ INVITATION_CODE = os.environ.get('INVITATION_CODE', None) # Auth #=============================== -OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID', None) +DISABLE_LOCAL_AUTH = booleanize(os.environ.get('DISABLE_LOCAL_AUTH', False)) + +OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID', None) if OIDC_RP_CLIENT_ID: