Skip to content

Commit

Permalink
Merge branch 'feature/SHIBUI-1031' of bitbucket.org:unicon/shib-idp-u…
Browse files Browse the repository at this point in the history
…i into feature/SHIBUI-1031
  • Loading branch information
Bill Smith committed Dec 13, 2018
2 parents d09f269 + e0b856c commit b30e6c1
Show file tree
Hide file tree
Showing 21 changed files with 239 additions and 92 deletions.
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ dependencies {

//JSON schema validator
compile 'org.sharegov:mjson:1.4.1'

// CSV file support
compile 'com.opencsv:opencsv:4.4'
}

def generatedSrcDir = new File(buildDir, 'generated/src/main/java')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package edu.internet2.tier.shibboleth.admin.ui.service

import com.opencsv.CSVReader
import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration
import edu.internet2.tier.shibboleth.admin.ui.security.model.Role
import edu.internet2.tier.shibboleth.admin.ui.security.model.User
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository
import groovy.util.logging.Slf4j
import org.springframework.boot.context.event.ApplicationStartedEvent
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component

import javax.transaction.Transactional

@Component
@Slf4j
class UserBootstrap {
private final ShibUIConfiguration shibUIConfiguration
private final UserRepository userRepository
private final RoleRepository roleRepository

UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository) {
this.shibUIConfiguration = shibUIConfiguration
this.userRepository = userRepository
this.roleRepository = roleRepository
}

@Transactional
@EventListener
void bootstrapUsersAndRoles(ApplicationStartedEvent e) {
if (shibUIConfiguration.userBootstrapResource) {
log.info("configuring users from ${shibUIConfiguration.userBootstrapResource.URI}")
new CSVReader(new InputStreamReader(shibUIConfiguration.userBootstrapResource.inputStream)).each { it ->
def (username, password, firstName, lastName, roleName) = it
def role = roleRepository.findByName(roleName).orElse(roleRepository.save(new Role(name: roleName)))
def user = userRepository.findByUsername(username).orElse(new User(username: username)).with {
it.password = password
it.firstName = firstName
it.lastName = lastName
it.roles.add(role)
it
}
userRepository.save(user)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;

import java.util.List;
import java.util.Optional;

@Configuration
@ConfigurationProperties(prefix = "shibui")
Expand All @@ -17,4 +19,19 @@ public class ShibUIConfiguration {
* sources from using attributes that they might not have the rights to use.
*/
private List<String> protectedAttributeNamespaces;

/**
* A Resource containing a CSV of users to bootstrap into the system. Currently, this must be in format
*
* <code>
* username,password,firstName,lastName,role
* </code>
*
* Note that the password must be encrypted in the file. Ensure that you prepend the encoder to the value, e.g.
*
* <code>
* {bcrypt}$2a$10$ssM2LpFqceRQ/ta0JehGcu0BawFQDbxjQGSyVmKS6qa09hHLigtAO
* </code>
*/
private Resource userBootstrapResource;
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,22 @@ public List<User> getAll() {
}

@Transactional(readOnly = true)
@GetMapping("/user/{username}")
@GetMapping("/users/{username}")
public ResponseEntity<?> getOne(@PathVariable String username) {
return ResponseEntity.ok(findUserOrThrowHttp404(username));
}

@Transactional
@DeleteMapping("/user/{username}")
@DeleteMapping("/users/{username}")
public ResponseEntity<?> deleteOne(@PathVariable String username) {
User user = findUserOrThrowHttp404(username);
userRepository.delete(user);
return ResponseEntity.noContent().build();
}


private User findUserOrThrowHttp404(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username)));
}

@Transactional
@PostMapping("/user")
ResponseEntity<?> saveUser(@RequestParam User user) {
@PostMapping("/users")
ResponseEntity<?> saveOne(@RequestParam User user) {
Optional<User> persistedUser = userRepository.findByUsername(user.getUsername());
if (persistedUser.isPresent()) {
return ResponseEntity
Expand All @@ -80,8 +74,8 @@ ResponseEntity<?> saveUser(@RequestParam User user) {
}

@Transactional
@PutMapping("/user/{username}")
ResponseEntity<?> updateUser(@PathVariable(value = "username") String username, @RequestParam User user) {
@PutMapping("/users/{username}")
ResponseEntity<?> updateOne(@PathVariable(value = "username") String username, @RequestParam User user) {
Optional<User> userSearchResult = userRepository.findByUsername(username);
if (!userSearchResult.isPresent()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
Expand All @@ -97,4 +91,9 @@ ResponseEntity<?> updateUser(@PathVariable(value = "username") String username,
User savedUser = userRepository.save(persistedUser);
return ResponseEntity.ok(savedUser);
}

private User findUserOrThrowHttp404(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,6 @@
"@type",
"content"
],
"anyOf": [
{
"properties": {
"@type": {
"enum": [
"Regex"
]
}
},
"required": [
"@type",
"content",
"match"
]
},
{
"properties": {
"@type": {
"enum": [
"MetadataQueryProtocol"
]
}
},
"required": [
"@type",
"content"
]
}
],
"properties": {
"@type": {
"title": "label.md-request-type",
Expand Down
10 changes: 7 additions & 3 deletions backend/src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ label.metadata-provider-type=Metadata Provider Type
label.metadata-provider-name=Metadata Provider Name
label.select-metadata-type=Select a metadata provider type
label.metadata-provider-status=Metadata Provider Status
label.enable-provider-upon-saving=If checkbox is clicked, the metadata provider is enabled for integration with the IdP
label.enable-provider-upon-saving=Enable Metadata Provider?
label.certificate-type=Type

label.metadata-file=Metadata File
Expand Down Expand Up @@ -391,6 +391,10 @@ message.org-displayName-required=Organization Name is required.
message.org-url-required=Organization Name is required.
message.org-incomplete=These three fields must all be entered if any single field has a value.

message.type-required=Missing required property: Type
message.match-required=Missing required property: Match
message.value-required=Missing required property: Value

message.conflict=Conflict
message.data-version-contention=Data Version Contention
message.contention-new-version=A newer version of this metadata source has been saved. Below are a list of changes. You can use your changes or their changes.
Expand Down Expand Up @@ -437,7 +441,7 @@ tooltip.assertion-consumer-service-location-binding=Assertion Consumer Service L
tooltip.mark-as-default=Mark as Default
tooltip.protocol-support-enumeration=Protocol Support Enumeration
tooltip.nameid-format=Content is name identifier format which is added to all the applicable roles of the entities which match any of the following <Entity> or {{<ConditionRef>}}elements.
tooltip.enable-this-service-upon-saving=Enable this service upon saving
tooltip.enable-this-service-upon-saving=If checkbox is clicked, the metadata provider is enabled for integration with the IdP
tooltip.authentication-requests-signed=Authentication Requests Signed
tooltip.want-assertions-signed=Want Assertions Signed
tooltip.certificate-name=Certificate Name
Expand Down Expand Up @@ -523,7 +527,7 @@ tooltip.source-directory=Convenience mechanism for wiring a FilesystemLoadSaveMa
tooltip.remove-idle-entity-data=Flag indicating whether idle metadata should be removed.

tooltip.do-resolver-initialization=Initialize this resolver? In the case of Filesystem resolvers, this will cause the system to read the file and index the resolver.
tooltip.md-request-type=Options are 1) Metadata Query Protocol, 2) Template, 3) Regex.
tooltip.md-request-type=Options are 1) Metadata Query Protocol, 2) Regex.
tooltip.md-request-value=Content of the element.
tooltip.transform-ref=A reference to a transform function for the entityID. If used, the child element must be empty.
tooltip.encoding-style=Determines whether and how the entityID value will be URL encoded prior to replacement. Allowed values are: 1) "none" - no encoding is performed, 2) "form" - encoded using URL form parameter encoding (for query parameters), 3) "path" - encoded using URL path encoding, or 4) "fragment" - encoded using URL fragment encoding. The precise definition of these terms is defined in the documentation for the methods of the Guava library\u0027s UrlEscapers class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ActiveProfiles
import spock.lang.Specification

Expand Down Expand Up @@ -48,6 +49,7 @@ class UsersControllerIntegrationTests extends Specification {
result.body.errorMessage == 'User with username [bogus] not found'
}
@DirtiesContext
def 'DELETE ONE existing user'() {
when: 'GET request is made for one existing user'
def result = this.restTemplate.getForEntity("$RESOURCE_URI/admin", Map)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package edu.internet2.tier.shibboleth.admin.ui.service

import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration
import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration
import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration
import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration
import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.core.io.ClassPathResource
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification

@DataJpaTest
@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, ShibUIConfiguration])
@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"])
@EntityScan(["edu.internet2.tier.shibboleth.admin.ui", "edu.internet2.tier.shibboleth.admin.ui.security.model"])
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
class UserBootstrapTests extends Specification {
@Autowired
ShibUIConfiguration shibUIConfiguration

@Autowired
UserRepository userRepository

@Autowired
RoleRepository roleRepository

def "simple test"() {
setup:
shibUIConfiguration.userBootstrapResource = new ClassPathResource('/conf/1044.csv')
def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository)

when:
userBootstrap.bootstrapUsersAndRoles(null)

then:
noExceptionThrown()
assert userRepository.findAll().size() == 2
assert roleRepository.findAll().size() == 2
}
}
2 changes: 2 additions & 0 deletions backend/src/test/resources/conf/1044.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"user1","password1","firstName1","lastName1","ROLE_ADMIN"
"user2","password2","firstName2","lastName2","ROLE_USER"
4 changes: 2 additions & 2 deletions ui/src/app/metadata/domain/service/resolver.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class ResolverService {
headers: new HttpHeaders().set('Content-Type', 'application/xml'),
params: new HttpParams().set('spName', name)
}).pipe(catchError(error => {
return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error}` });
return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error.errorMessage}` });
}));
}

Expand All @@ -56,7 +56,7 @@ export class ResolverService {
headers: new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'),
params: new HttpParams().set('spName', name)
}).pipe(catchError(error => {
return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error}` });
return throwError({ errorCode: error.status, errorMessage: `Unable to upload file ... ${error.error.errorMessage}` });
}));
}

Expand Down
8 changes: 7 additions & 1 deletion ui/src/app/metadata/filter/action/filter.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum FilterActionTypes {
SELECT_FILTER_TYPE = '[Filter] Select Filter Type',
UPDATE_FILTER = '[Filter] Update Filter',
CANCEL_CREATE_FILTER = '[Filter] Cancel Create Filter',
CLEAR_FILTER = '[Filter] Clear Filter',
LOAD_ENTITY_PREVIEW = '[Filter] Load Preview data',
LOAD_ENTITY_PREVIEW_SUCCESS = '[Filter] Load Preview data success',
LOAD_ENTITY_PREVIEW_ERROR = '[Filter] Load Preview data error'
Expand Down Expand Up @@ -38,6 +39,10 @@ export class CancelCreateFilter implements Action {
readonly type = FilterActionTypes.CANCEL_CREATE_FILTER;
}

export class ClearFilter implements Action {
readonly type = FilterActionTypes.CLEAR_FILTER;
}

export class UpdateFilterChanges implements Action {
readonly type = FilterActionTypes.UPDATE_FILTER;

Expand All @@ -57,4 +62,5 @@ export type FilterActionsUnion =
| CancelCreateFilter
| LoadEntityPreview
| LoadEntityPreviewSuccess
| LoadEntityPreviewError;
| LoadEntityPreviewError
| ClearFilter;
6 changes: 4 additions & 2 deletions ui/src/app/metadata/filter/container/new-filter.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,21 @@ export class NewFilterComponent implements OnDestroy, OnInit {
this.model = {};

this.definition$ = this.store.select(fromFilter.getFilterType).pipe(
takeUntil(this.ngUnsubscribe),
filter(t => !!t),
map(t => MetadataFilterTypes[t])
);
this.schema$ = this.definition$.pipe(
takeUntil(this.ngUnsubscribe),
filter(d => !!d),
switchMap(d => {
return this.schemaService.get(d.schema);
return this.schemaService.get(d.schema).pipe(takeUntil(this.ngUnsubscribe));
}),
shareReplay()
);

this.validators$ = this.definition$.pipe(
takeUntil(this.ngUnsubscribe),
withLatestFrom(this.store.select(fromFilter.getFilterNames)),
map(([definition, names]) => definition.getValidators(names))
);
Expand Down Expand Up @@ -107,7 +110,6 @@ export class NewFilterComponent implements OnDestroy, OnInit {
}

save(): void {
console.log(this.filter);
this.store.dispatch(new AddFilterRequest(this.filter));
}

Expand Down
8 changes: 7 additions & 1 deletion ui/src/app/metadata/filter/effect/collection.effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { removeNulls, array_move } from '../../../shared/util';
import { EntityAttributesFilterEntity } from '../../domain/entity/filter/entity-attributes-filter';
import { MetadataFilterService } from '../../domain/service/filter.service';
import { SelectProviderRequest } from '../../provider/action/collection.action';
import { UpdateFilterChanges } from '../action/filter.action';
import { UpdateFilterChanges, ClearFilter } from '../action/filter.action';

/* istanbul ignore next */
@Injectable()
Expand Down Expand Up @@ -112,6 +112,12 @@ export class FilterCollectionEffects {
map(([filter, provider]) => new SelectProviderRequest(provider))
);

@Effect()
addFilterSuccessResetState$ = this.actions$.pipe(
ofType<AddFilterSuccess>(FilterCollectionActionTypes.ADD_FILTER_SUCCESS),
map(() => new ClearFilter())
);

@Effect()
updateFilter$ = this.actions$.pipe(
ofType<UpdateFilterRequest>(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST),
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/metadata/filter/model/nameid.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MetadataFilter } from '../../domain/model';
import { NameIDFormatFilterEntity } from '../../domain/entity/filter/nameid-format-filter';

export const NameIDFilter: FormDefinition<MetadataFilter> = {
label: 'NameIDFilter',
label: 'NameIDFormat',
type: 'NameIDFormat',
schema: '/api/ui/NameIdFormatFilter',
getEntity(filter: MetadataFilter): NameIDFormatFilterEntity {
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/metadata/filter/reducer/filter.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function reducer(state = initialState, action: FilterActionsUnion): Filte
}
};
}
case FilterActionTypes.CLEAR_FILTER:
case FilterActionTypes.CANCEL_CREATE_FILTER: {
return {
...initialState
Expand Down
Loading

0 comments on commit b30e6c1

Please sign in to comment.