spring-cloud-dataflow icon indicating copy to clipboard operation
spring-cloud-dataflow copied to clipboard

feat: Configurable OAuth2 login page

Open klopfdreh opened this issue 2 years ago • 19 comments

Address the issue: https://github.com/spring-cloud/spring-cloud-dataflow-ui/issues/1887

When OAuth2 is enabled and there are client-registrations with authorization-grand-type authorization_code all those are going to be listed within the Angular-Login, now. Also when you logout and login again you are redirected correctly.

You can see the appearance here: https://github.com/spring-cloud/spring-cloud-dataflow-ui/pull/1923

❗ There is one issue left I couldn't solve yet. When I fill the getAuthenticatedPaths() the loginUrl can be accessed because of to many redirects - even if this.authorizationProperties.getPermitAllPaths().add(authorizationProperties.getLoginUrl()); is added. I am going to mark this as a comment.

To test OAuth2 locally I used the following configuration (important properties are login-url: "/dashboard/#/authentication-required" and the client registration user_login):

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: http://localhost:8080/api/introspect
          client-id: 'vsrc13725ClientId'
          client-secret: 'vsrc13725ClientId'
      client:
        registration:
          user_login:
            provider: user_login
            client-id: 'vsrc13725ClientId'
            client-secret: 'vsrc13725Secret'
            client-authentication-method: client_secret_basic
            redirect-uri: http://localhost:9393/login/oauth2/code/user_login
            authorization-grant-type: authorization_code
            scope:
              - read
              - write
              - admin
          program_login:
            provider: program_login
            client-id: 'vsrc13725ClientId'
            client-secret: 'vsrc13725Secret'
            authorization-grant-type: client_credentials
            scope:
              - read
              - write
              - admin
        provider:
          user_login:
            token-uri: http://localhost:8080/oauth/token
            user-info-uri: http://localhost:8080/api/users/me
            user-name-attribute: email
            authorization-uri: http://localhost:8080/oauth/authorize
          program_login:
            token-uri: http://localhost:8080/oauth/token
  cloud:
    dataflow:
      security:
        authorization:
          defaultProviderId: program_login
          provider-role-mappings:
            user_login:
              map-oauth-scopes: true
              parse-oauth-scope-path-parts: false
              role-mappings:
                ROLE_CREATE: 'write'
                ROLE_DEPLOY: 'write'
                ROLE_DESTROY: 'write'
                ROLE_MANAGE: 'admin'
                ROLE_MODIFY: 'write'
                ROLE_SCHEDULE: 'write'
                ROLE_VIEW: 'read'
            program_login:
              map-oauth-scopes: true
              parse-oauth-scope-path-parts: false
              role-mappings:
                ROLE_CREATE: 'write'
                ROLE_DEPLOY: 'write'
                ROLE_DESTROY: 'write'
                ROLE_MANAGE: 'admin'
                ROLE_MODIFY: 'write'
                ROLE_SCHEDULE: 'write'
                ROLE_VIEW: 'read'
          login-url: "/dashboard/#/authentication-required"
          permit-all-paths: "/management/health,/management/health/liveness,/management/health/readiness,/management/info,/security/info,/assets/**,/dashboard/logout-success-oauth.html,/favicon.ico"
          rules:
            # About

            - GET    /about                          => hasRole('ROLE_VIEW')

            # Audit

            - GET /audit-records                     => hasRole('ROLE_VIEW')
            - GET /audit-records/**                  => hasRole('ROLE_VIEW')

            # Boot Endpoints

            - GET /management/**                  => hasRole('ROLE_MANAGE')

            # Apps

            - GET    /apps                           => hasRole('ROLE_VIEW')
            - GET    /apps/**                        => hasRole('ROLE_VIEW')
            - DELETE /apps/**                        => hasRole('ROLE_DESTROY')
            - POST   /apps                           => hasRole('ROLE_CREATE')
            - POST   /apps/**                        => hasRole('ROLE_CREATE')
            - PUT    /apps/**                        => hasRole('ROLE_MODIFY')

            # Completions

            - GET /completions/**                    => hasRole('ROLE_VIEW')

            # Job Executions & Batch Job Execution Steps && Job Step Execution Progress

            - GET    /jobs/executions                => hasRole('ROLE_VIEW')
            - PUT    /jobs/executions/**             => hasRole('ROLE_MODIFY')
            - GET    /jobs/executions/**             => hasRole('ROLE_VIEW')
            - GET    /jobs/thinexecutions            => hasRole('ROLE_VIEW')

            # Batch Job Instances

            - GET    /jobs/instances                 => hasRole('ROLE_VIEW')
            - GET    /jobs/instances/*               => hasRole('ROLE_VIEW')

            # Running Applications

            - GET    /runtime/streams                => hasRole('ROLE_VIEW')
            - GET    /runtime/streams/**             => hasRole('ROLE_VIEW')
            - GET    /runtime/apps                   => hasRole('ROLE_VIEW')
            - GET    /runtime/apps/**                => hasRole('ROLE_VIEW')

            # Stream Definitions

            - GET    /streams/definitions            => hasRole('ROLE_VIEW')
            - GET    /streams/definitions/*          => hasRole('ROLE_VIEW')
            - GET    /streams/definitions/*/related  => hasRole('ROLE_VIEW')
            - GET    /streams/definitions/*/applications  => hasRole('ROLE_VIEW')
            - POST   /streams/definitions            => hasRole('ROLE_CREATE')
            - DELETE /streams/definitions/*          => hasRole('ROLE_DESTROY')
            - DELETE /streams/definitions            => hasRole('ROLE_DESTROY')

            # Stream Deployments

            - DELETE /streams/deployments/*          => hasRole('ROLE_DEPLOY')
            - DELETE /streams/deployments            => hasRole('ROLE_DEPLOY')
            - POST   /streams/deployments/**         => hasRole('ROLE_MODIFY')
            - GET    /streams/deployments/**         => hasRole('ROLE_VIEW')

            # Stream Validations

            - GET /streams/validation/               => hasRole('ROLE_VIEW')
            - GET /streams/validation/*              => hasRole('ROLE_VIEW')

            # Stream Logs
            - GET /streams/logs/**                    => hasRole('ROLE_VIEW')

            # Task Definitions

            - POST   /tasks/definitions              => hasRole('ROLE_CREATE')
            - DELETE /tasks/definitions/*            => hasRole('ROLE_DESTROY')
            - DELETE /tasks/definitions              => hasRole('ROLE_DESTROY')
            - GET    /tasks/definitions              => hasRole('ROLE_VIEW')
            - GET    /tasks/definitions/*            => hasRole('ROLE_VIEW')

            # Task Executions

            - GET    /tasks/executions               => hasRole('ROLE_VIEW')
            - GET    /tasks/executions/*             => hasRole('ROLE_VIEW')
            - POST   /tasks/executions               => hasRole('ROLE_DEPLOY')
            - POST   /tasks/executions/*             => hasRole('ROLE_DEPLOY')
            - DELETE /tasks/executions/*             => hasRole('ROLE_DESTROY')
            - DELETE /tasks/executions               => hasRole('ROLE_DESTROY')
            - GET    /tasks/info/*                   => hasRole('ROLE_VIEW')

            # Task Schedules

            - GET    /tasks/schedules                => hasRole('ROLE_VIEW')
            - GET    /tasks/schedules/*              => hasRole('ROLE_VIEW')
            - GET    /tasks/schedules/instances      => hasRole('ROLE_VIEW')
            - GET    /tasks/schedules/instances/*    => hasRole('ROLE_VIEW')
            - POST   /tasks/schedules                => hasRole('ROLE_SCHEDULE')
            - DELETE /tasks/schedules/*              => hasRole('ROLE_SCHEDULE')
            - DELETE /tasks/schedules                => hasRole('ROLE_SCHEDULE')

            # Task Platform Account List */

            - GET    /tasks/platforms                => hasRole('ROLE_VIEW')

            # Task Validations

            - GET    /tasks/validation/               => hasRole('ROLE_VIEW')
            - GET    /tasks/validation/*              => hasRole('ROLE_VIEW')

            # Task Logs
            - GET /tasks/logs/*                       => hasRole('ROLE_VIEW')

            # Task Ctr
            - GET    /tasks/ctr/*                     => hasRole('ROLE_VIEW')

            # Tools

            - POST   /tools/**                       => hasRole('ROLE_VIEW')

and in the server I switched to my locally build UI with:

pom.xml in spring-cloud-dataflow-server

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-dataflow-ui</artifactId>
			<version>${project.version}</version>
			<exclusions>
				<exclusion>
					<artifactId>spring-cloud-dataflow-ui</artifactId>
					<groupId>org.springframework.cloud</groupId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<artifactId>spring-cloud-dataflow-ui</artifactId>
			<groupId>org.springframework.cloud</groupId>
			<version>3.3.4-SNAPSHOT</version>
		</dependency>

klopfdreh avatar May 19 '23 10:05 klopfdreh

Hey @onobc - I am not that good in Spring Security configuration, but I thought when I call permitAll() first and after this authenticated() for a more concrete page pattern, that those paths are going to be whitelisted. This seems to fail in my case - can you or someone of the SCDF team help me out there?

Other than that the OAuth2 Angular login is working.

klopfdreh avatar May 19 '23 11:05 klopfdreh

I also actually don't know why this test is failing 😨

Bildschirmfoto 2023-05-19 um 13 18 10

Could it be that I changed the AboutController and the ClientRegistrations are provided in the SecurityInfo response, now?

klopfdreh avatar May 19 '23 11:05 klopfdreh

Hi @klopfdreh , I will try to take a look sometime today.

onobc avatar May 19 '23 15:05 onobc

Thanks a lot!

klopfdreh avatar May 19 '23 15:05 klopfdreh

Hey @onobc - I think I found a solution - see: https://github.com/klopfdreh/spring-cloud-dataflow/pull/2

The UI already checks the security in the root path. So I adjusted the settings so that the login page is allowed.

The login-url I adjusted a bit: /dashboard/index.html#/authentication-required

with this you can now login without having an impact on other services. But please verify that I did not missed anything.

Here are some screenshots what happens:

  1. I entered the url http://localhost:9393/dashboard Bildschirmfoto 2023-05-20 um 22 59 30
  2. The new dashboard login page with the registrations is shown Bildschirmfoto 2023-05-20 um 22 59 40
  3. When I click on a registration I am redirected to the OAuth server's consent page Bildschirmfoto 2023-05-20 um 22 59 49
  4. When I allowed the scopes to the backend I can see the dashboard Bildschirmfoto 2023-05-20 um 23 00 05

klopfdreh avatar May 20 '23 20:05 klopfdreh

One further good thing to mention is that you still can use the spring-security login page if you want to - I just forgot about this!

klopfdreh avatar May 20 '23 21:05 klopfdreh

@klopfdreh thanks for the updates. I did not get a chance to get to this yesterday. I will be sure to dedicate some time early this work week to thoroughly review the proposal. Again, thank you for all you do.

onobc avatar May 20 '23 21:05 onobc

No worries - I am glad to help a bit in my free time.

klopfdreh avatar May 20 '23 21:05 klopfdreh

@onobc - Build is repaired 👍 - I am adjust the skipper to work the same way in SkipperOAuthSecurityConfiguration.

klopfdreh avatar May 21 '23 19:05 klopfdreh

The last thing I could think about is what clientRegistrations are returned from the SecurityController - currently the registrations are filtered for those which have AuthorizationGrantType set to authorization_code as those are the important to be used for the user OAuth2 login within the UI.

The standard spring-security OAuth2 login form also filters for authorization_code.

Currently I would stick to this, but this could be improved that the all clientRegistrations are returned as complex objects and the UI filters for `authorization_code.

klopfdreh avatar May 21 '23 19:05 klopfdreh

I tested the toggle between

login-url: "/login" (default spring-security oauth2 login)

and

login-url: "/dashboard/index.html#/authentication-required" (angular login)

both is working now.

klopfdreh avatar May 21 '23 20:05 klopfdreh

@klopfdreh Thank you for this contribution. We will be able to give it the attention it deserves once we have 2.11.0 out the door.

corneil avatar May 24 '23 09:05 corneil

All right! If there are any further questions just ping me. 👍

klopfdreh avatar May 24 '23 09:05 klopfdreh

Let me know when you are able to have a look at this change. Might be cool to get a fancy Angular Login page instead of simple text. 😄

klopfdreh avatar Nov 09 '23 22:11 klopfdreh

Resolved the conflicts to integrate the updates for the AboutController. Other than that the UI looks good to me. thank you @klopfdreh

claudiahub avatar Nov 10 '23 08:11 claudiahub

@klopfdreh we will need to wait post 2.11.2 for this to get in. We are busy fixing fallout from the big move to support Boot3. Once the dust settles we will get this in.

Thanks for your patience.

onobc avatar Nov 10 '23 15:11 onobc

@onobc - just a short reminder - would be nice to have this implemented as we can adjust our CSP settings to not allow any content from the bootstrap CDN anymore 👍 (zero trust)

Important hint: A review needs to be done before merging: https://github.com/spring-cloud/spring-cloud-dataflow/pull/5350#discussion_r1209886862

klopfdreh avatar Feb 05 '24 08:02 klopfdreh

Hi @klopfdreh - thanks for the reminder. Not exactly sure when we will get to this but it is on the front burner again w/ your ping :)

onobc avatar Feb 07 '24 16:02 onobc

Awesome! 😄

klopfdreh avatar Feb 07 '24 17:02 klopfdreh