Skip to content

Commit

Permalink
Feature/21 builtin users (#27)
Browse files Browse the repository at this point in the history
* #21 
 - divided base and jwt config
 - application.properties replaced by application.yaml
 - loading knownUsers objects from config
 - DummyAuthenticationProvider.scala -> ConfigUsersAuthenticationProvider (actually consumes config)
 -  basic UserConfig validation on app startup
   - validates groups presence, may be empty
   - userconfig validation for duplicates added
 - introduced sealed trait ConfigValidationResult (with container impl ConfigValidationError and emptyConfigValidationSuccess).
 - ConfigValidationResult helper methods ( .merge(CVR), .getErrors, ConfigValidationError creation shorthand)
 - ConfigValidationResult.throwOnErrors
 - unit tests added
  • Loading branch information
dk1844 authored May 2, 2023
1 parent 99c250d commit b78160b
Show file tree
Hide file tree
Showing 20 changed files with 711 additions and 110 deletions.
24 changes: 0 additions & 24 deletions service/src/main/resources/application.properties

This file was deleted.

60 changes: 60 additions & 0 deletions service/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
logingw:
rest:
# Rest General Config
jwt:
exp-time: 4
alg-name: "RS256"
config:
some-key: "BETA"

# Users (config-defined)
users:
known-users:
-
username: "user1"
password: "password1"
groups:
-
username: "TestUser"
password: "password123"
email: "[email protected]"
groups:
- "groupA"
- "groupB"

# Rest Auth Config (AD)
auth:
ad:
ldap:
domain: "some.domain.com"
url: "ldaps://some.domain.com:636/"
search-filter: "(samaccountname={1})"

# App Config
spring:
application:
name: "login-gateway"
jmx:
enabled: true
server:
port: 9090

# Health Check Config + JMX Config
springdoc:
show-actuator: true

management:
health:
ldap:
#TODO: Enable Ldap check for actuator/health when fully Implemented - issue #34
enabled: "false"
endpoints:
jmx:
exposure:
include: "health"
web:
exposure:
include: "health"
endpoint:
health:
show-details: "never"
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ import org.springframework.context.annotation._
`type` = SecuritySchemeType.HTTP,
scheme = "basic"
)
@Configuration
@SpringBootApplication()
@PropertySource(Array("classpath:application.properties"))
@ConfigurationPropertiesScan(Array("za.co.absa.logingw.rest.config"))
@ConfigurationPropertiesScan(Array("za.co.absa.logingw.rest.config")) // look for configuration in this package (not related to path in config file)
class Application extends SpringBootServletInitializer {

override def configure(application: SpringApplicationBuilder): SpringApplicationBuilder =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.logingw.rest

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.{Bean, Configuration}
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import za.co.absa.logingw.rest.config.{ActiveDirectoryLDAPConfig, UsersConfig}
import za.co.absa.logingw.rest.provider.ConfigUsersAuthenticationProvider
import za.co.absa.logingw.rest.provider.ad.ldap.ActiveDirectoryLDAPAuthenticationProvider

@Configuration
class AuthManagerConfig @Autowired()(
// TODO make it autowired but only if in config AD LDAP provider is set up (#28)
usersConfig: UsersConfig,
adLDAPConfig: ActiveDirectoryLDAPConfig
){

@Bean
def authManager(http: HttpSecurity): AuthenticationManager = {
val authenticationManagerBuilder = http.getSharedObject(classOf[AuthenticationManagerBuilder])

// TODO: take which providers and in which order to use from config (#28)
authenticationManagerBuilder
// if it is not null, on auth failure infinite recursion happens
.parentAuthenticationManager(null)
// currently, comment out or reorder the auth providers you want to use - #28
.authenticationProvider(
new ConfigUsersAuthenticationProvider(usersConfig)
)
.authenticationProvider(
new ActiveDirectoryLDAPAuthenticationProvider(adLDAPConfig)
)
.build
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@
package za.co.absa.logingw.rest

import org.springframework.context.annotation.{Bean, Configuration}
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
import za.co.absa.logingw.rest.config.ActiveDirectoryLDAPConfig
import za.co.absa.logingw.rest.provider.DummyAuthenticationProvider
import za.co.absa.logingw.rest.provider.ad.ldap.ActiveDirectoryLDAPAuthenticationProvider

@Configuration
@EnableWebSecurity
Expand Down Expand Up @@ -56,28 +51,6 @@ class SecurityConfig {
http.build()
}

@Bean
def authManager(http: HttpSecurity): AuthenticationManager = {
val authenticationManagerBuilder = http.getSharedObject(classOf[AuthenticationManagerBuilder])

// TODO make it autowired but only if in confid AD LDAP provider is set up (#28)
val adLDAPConfig = ActiveDirectoryLDAPConfig(
"some.domain.com",
"ldaps://some.domain.com:636/",
"(samaccountname={1})"
)

// TODO: take which providers and in which order to use from config (#28)
authenticationManagerBuilder
// if it is not null, on auth failure infinite recursion happens
.parentAuthenticationManager(null)
.authenticationProvider(
new ActiveDirectoryLDAPAuthenticationProvider(adLDAPConfig)
)
.authenticationProvider(
new DummyAuthenticationProvider()
)
.build
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,5 @@ import org.springframework.boot.context.properties.{ConfigurationProperties, Con
@ConstructorBinding
@ConfigurationProperties(prefix = "logingw.rest.config")
case class BaseConfig(
algName: String,
expTime: Int,
someKey: String = "BETA"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.logingw.rest.config

import io.jsonwebtoken.SignatureAlgorithm
import org.springframework.boot.context.properties.{ConfigurationProperties, ConstructorBinding}
import za.co.absa.logingw.rest.config.validation.{ConfigValidatable, ConfigValidationException, ConfigValidationResult}
import za.co.absa.logingw.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess}

import javax.annotation.PostConstruct
import scala.util.{Failure, Success, Try}

@ConstructorBinding
@ConfigurationProperties(prefix = "logingw.rest.jwt")
case class JwtConfig(
algName: String,
expTime: Int
) extends ConfigValidatable {

@PostConstruct
def init(): Unit = {
this.validate().throwOnErrors()
}

/** May throw ConfigValidationException or IllegalArgumentException */
override def validate(): ConfigValidationResult = {

val algValidation = Try {
SignatureAlgorithm.valueOf(algName)
} match {
case Success(_) => ConfigValidationSuccess
case Failure(e: IllegalArgumentException) if e.getMessage.contains("No enum constant") =>
ConfigValidationError(ConfigValidationException(s"Invalid algName '$algName' was given."))
case Failure(e) => throw e
}

val expTimeResult = if (expTime < 1) {
ConfigValidationError(ConfigValidationException("expTime must be positive (hours)"))
} else ConfigValidationSuccess

algValidation.merge(expTimeResult)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.logingw.rest.config

import org.springframework.boot.context.properties.{ConfigurationProperties, ConstructorBinding}
import za.co.absa.logingw.rest.config.validation.ConfigValidationResult.{ConfigValidationError, ConfigValidationSuccess}
import za.co.absa.logingw.rest.config.validation.{ConfigValidatable, ConfigValidationException, ConfigValidationResult}

import javax.annotation.PostConstruct

@ConstructorBinding
@ConfigurationProperties(prefix = "logingw.rest.users")
case class UsersConfig(
knownUsers: Array[UserConfig],
) extends ConfigValidatable {
lazy val knownUsersMap: Map[String, UserConfig] = knownUsers
.map { entry => (entry.username, entry) }
.toMap

// todo validation is done using a custom trait/method -- Issue #24 validation
// Until is resolved https://github.com/spring-projects/spring-boot/issues/33669
override def validate(): ConfigValidationResult = {
Option(knownUsers).map { existingKnownUsers =>

val kuDuplicatesResult = {
val groupedByUsers = existingKnownUsers.groupBy(_.username)
if (groupedByUsers.size < existingKnownUsers.size) {
val duplicates = groupedByUsers.filter { case (username, configs) => configs.length > 1 }.keys
ConfigValidationError(ConfigValidationException(s"knownUsers contain duplicates, duplicated usernames: ${duplicates.mkString(", ")}"))
} else ConfigValidationSuccess
}

val usersResult = existingKnownUsers.map(_.validate()).toList

(kuDuplicatesResult :: usersResult)
.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)

}.getOrElse(ConfigValidationError(ConfigValidationException("knownUsers is missing")))
}

@PostConstruct
def init(): Unit = {
this.validate().throwOnErrors()
}
}

@ConstructorBinding
case class UserConfig(
username: String,
password: String,
email: String, // may be null
groups: Array[String]
) extends ConfigValidatable {

override def toString: String = {
s"UserConfig($username, $password, $email, ${Option(groups).map(_.toList)})"
}

override def validate(): ConfigValidationResult = {
val results = Seq(
Option(username)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("username is empty"))),

Option(password)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("password is empty"))),

Option(groups)
.map(_ => ConfigValidationSuccess)
.getOrElse(ConfigValidationError(ConfigValidationException("groups are missing (empty groups are allowed)!")))

)

results.foldLeft[ConfigValidationResult](ConfigValidationSuccess)(ConfigValidationResult.merge)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.logingw.rest.config.validation

trait ConfigValidatable {
def validate(): ConfigValidationResult

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2023 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.logingw.rest.config.validation

case class ConfigValidationException(msg: String) extends Exception(msg)

Loading

0 comments on commit b78160b

Please sign in to comment.