Skip to content

Commit

Permalink
Merge branch 'v2-master' into v3-master
Browse files Browse the repository at this point in the history
* v2-master:
  Add support for view and edit profile for local user (#3883)
  Add fix to wait until delete indicator has been removed (#3889)
  FIx setup detection for local users (#3888)
  Ensure stepper buttons are always visible and content scrolls (#3890)
  E2E Test should run as user not admin (#3894)
  • Loading branch information
KlapTrap committed Sep 23, 2019
2 parents 9764251 + 39323bb commit 8058008
Show file tree
Hide file tree
Showing 20 changed files with 542 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ <h1>Edit User Profile</h1>
<mat-form-field>
<input matInput placeholder="Primary Email Address" formControlName="emailAddress">
</mat-form-field>
<p *ngIf="!(canChangePassword | async)">Current password is required when changing email address</p>
<p *ngIf="(canChangePassword | async)">Current password is required when changing email address or password</p>
<mat-form-field>
<p *ngIf="!(canChangePassword | async) && needsPasswordForEmailChange">Current password is required when changing email address</p>
<p *ngIf="(canChangePassword | async) && needsPasswordForEmailChange">Current password is required when changing email address or password</p>
<mat-form-field *ngIf="needsPasswordForEmailChange">
<input matInput placeholder="Current Password" type="password" formControlName="currentPassword" [required]="passwordRequired">
</mat-form-field>
<div *ngIf="(canChangePassword | async)" class="edit-profile__group">
<p>Change Password (Leave blank to keep current password)</p>
<mat-form-field *ngIf="!needsPasswordForEmailChange">
<input matInput placeholder="Current Password" type="password" formControlName="currentPassword" [required]="passwordRequired">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="New Password" type="password" formControlName="newPassword">
</mat-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class EditProfileInfoComponent implements OnInit, OnDestroy {

editProfileForm: FormGroup;

needsPasswordForEmailChange: boolean;

constructor(
private userProfileService: UserProfileService,
private fb: FormBuilder,
Expand All @@ -36,6 +38,8 @@ export class EditProfileInfoComponent implements OnInit, OnDestroy {
newPassword: '',
confirmPassword: '',
});

this.needsPasswordForEmailChange = false;
}

private sub: Subscription;
Expand All @@ -56,6 +60,9 @@ export class EditProfileInfoComponent implements OnInit, OnDestroy {
ngOnInit() {
this.userProfileService.fetchUserProfile();
this.userProfileService.userProfile$.pipe(first()).subscribe(profile => {
// UAA needs the user's password for email changes. Local user does not
// Both need it for password change
this.needsPasswordForEmailChange = (profile.origin === 'uaa');
this.profile = profile;
this.emailAddress = this.userProfileService.getPrimaryEmailAddress(profile);
this.editProfileForm.setValue({
Expand All @@ -76,7 +83,10 @@ export class EditProfileInfoComponent implements OnInit, OnDestroy {

onChanges() {
this.sub = this.editProfileForm.valueChanges.subscribe(values => {
const required = values.emailAddress !== this.emailAddress || values.newPassword.length;
// Old password is required if either email or new pw is specified (uaa)
// or only if new pw is specified (local account)
const required = this.needsPasswordForEmailChange ?
values.emailAddress !== this.emailAddress || values.newPassword.length : values.newPassword.length;
this.passwordRequired = !!required;
if (required !== this.lastRequired) {
this.lastRequired = required;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ <h1>User Profile</h1>
</div>
</app-page-header>
<div class="user-profile">
<app-no-content-message *ngIf="isError$ | async"
[icon]="'error'" [firstLine]="'An error occurred retrieving the User Profile'" [secondLine]="{
<app-no-content-message *ngIf="isError$ | async" [icon]="'error'"
[firstLine]="'An error occurred retrieving the User Profile'" [secondLine]="{
text: ''
}"></app-no-content-message>
<app-user-profile-banner *ngIf="userProfile$ | async as profile"
name="{{ profile.name.givenName }} {{ profile.name.familyName }}" email="{{ primaryEmailAddress$ | async }}">
name="{{ profile.name.givenName }} {{ profile.name.familyName }}" email="{{ primaryEmailAddress$ | async }}"
username="{{ profile.userName }}">
</app-user-profile-banner>
<div class="user-profile__info" *ngIf="userProfile$ | async as profile">
<div class="user-profile__content">
Expand All @@ -24,11 +25,20 @@ <h1>User Profile</h1>
<div class="app-metadata">
<div class="app-metadata__two-cols">
<app-metadata-item icon="person" label="User id">{{ profile.userName }}</app-metadata-item>
<app-metadata-item icon="title" label="Name">{{ profile.name.givenName }} {{ profile.name.familyName }}
</app-metadata-item>
<app-metadata-item icon="email" label="Email">{{ primaryEmailAddress$ | async }}</app-metadata-item>
<app-metadata-item *ngIf="(profile.name.givenName || profile.name.familyName); else noName" icon="title"
label="Name">{{ profile.name.givenName }} {{ profile.name.familyName }}</app-metadata-item>
<ng-template #noName>
<app-metadata-item icon="title" label="Name">No Name</app-metadata-item>
</ng-template>
<app-metadata-item *ngIf="primaryEmailAddress$ | async; else noEmail" icon="email" label="Email">
{{ primaryEmailAddress$ | async }}</app-metadata-item>
<ng-template #noEmail>
<app-metadata-item icon="email" label="Email">No Email Address</app-metadata-item>
</ng-template>
<app-metadata-item *ngIf="(this.userService.isAdmin$ | async)" icon="security" label="User Type">
Administrator</app-metadata-item>
</div>
<div class="app-metadata__two-cols">
<div class="app-metadata__two-cols" *ngIf="profile.origin === 'uaa'">
<app-metadata-item icon="date_range" label="Account Created">{{ profile.meta.created | date:'medium' }}
</app-metadata-item>
<app-metadata-item icon="date_range" label="Account Last Modified">
Expand Down Expand Up @@ -84,7 +94,7 @@ <h1>User Profile</h1>
</div>
</div>
</mat-card>
<mat-card>
<mat-card *ngIf="profile.origin === 'uaa'">
<mat-card-header>
<mat-card-title>Groups</mat-card-title>
</mat-card-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UserProfileInfo } from '../../../../../store/src/types/user-profile.typ
import { ConfirmationDialogConfig } from '../../../shared/components/confirmation-dialog.config';
import { ConfirmationDialogService } from '../../../shared/components/confirmation-dialog.service';
import { UserProfileService } from '../user-profile.service';
import { UserService } from '../../../core/user.service';

@Component({
selector: 'app-profile-info',
Expand Down Expand Up @@ -58,6 +59,7 @@ export class ProfileInfoComponent implements OnInit {
private userProfileService: UserProfileService,
private store: Store<DashboardOnlyAppState>,
private confirmDialog: ConfirmationDialogService,
public userService: UserService,
) {
this.isError$ = userProfileService.isError$;
this.userProfile$ = userProfileService.userProfile$;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
}
&__contents {
display: flex;
flex: 1;
flex: 1 1 auto;
height: 0;
min-height: 160px;
overflow-y: auto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
<div class="user-profile-banner__avatar">
<mat-icon>account_circle</mat-icon>
</div>
<div class="user-profile-banner__title">
<div class="user-profile-banner__title" *ngIf="(name || email); else noNameOrEmail">
<div>{{ name }}</div>
<div class="user-profile-banner__email">{{ email }}</div>
</div>
<ng-template #noNameOrEmail>
<div class="user-profile-banner__title">
<div>{{ username }}</div>
</div>
</ng-template>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ import { Component, OnInit, Input } from '@angular/core';
})
export class UserProfileBannerComponent implements OnInit {

@Input() name: string;
private uName: string;

@Input()
get name(): string { return this.uName; }
set name(name: string) {
this.uName = name.trim();
}

@Input() email: string;
@Input() username: string;

constructor() { }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class UserProfileEffect {
type: action.type,
} as EntityRequestAction;
this.store.dispatch(new StartRequestAction(apiAction));
return this.httpClient.get(`/pp/${proxyAPIVersion}/uaa/Users/${action.guid}`).pipe(
return this.httpClient.get(`/pp/${proxyAPIVersion}/users/${action.guid}`).pipe(
mergeMap((info: UserProfileInfo) => {
return [
new WrapperRequestActionSuccess({
Expand Down Expand Up @@ -89,7 +89,7 @@ export class UserProfileEffect {
headers['x-stratos-password'] = action.password;
}

return this.httpClient.put(`/pp/${proxyAPIVersion}/uaa/Users/${guid}`, action.profile, { headers }).pipe(
return this.httpClient.put(`/pp/${proxyAPIVersion}/users/${guid}`, action.profile, { headers }).pipe(
mergeMap((info: UserProfileInfo) => {
return [
new WrapperRequestActionSuccess({
Expand Down Expand Up @@ -122,7 +122,7 @@ export class UserProfileEffect {
'x-stratos-password': action.passwordChanges.oldPassword,
'x-stratos-password-new': action.passwordChanges.password
};
return this.httpClient.put(`/pp/${proxyAPIVersion}/uaa/Users/${guid}/password`, action.passwordChanges, { headers }).pipe(
return this.httpClient.put(`/pp/${proxyAPIVersion}/users/${guid}/password`, action.passwordChanges, { headers }).pipe(
switchMap((info: UserProfileInfo) => {
return [
new WrapperRequestActionSuccess({
Expand Down
4 changes: 4 additions & 0 deletions src/jetstream/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,10 @@ func (p *portalProxy) getLocalUser(userGUID string) (*interfaces.ConnectedUser,
}

var scopes []string
scopes = make([]string, 2)
scopes[0] = user.Scope
scopes[1] = "password.write"

uaaAdmin := (user.Scope == p.Config.ConsoleConfig.ConsoleAdminScope)
uaaEntry := &interfaces.ConnectedUser{
GUID: userGUID,
Expand Down
33 changes: 33 additions & 0 deletions src/jetstream/datastore/20190918092300_LocalUsersUpdates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package datastore

import (
"database/sql"

"bitbucket.org/liamstask/goose/lib/goose"
)

func init() {

RegisterMigration(20190918092300, "LocalUsersUpdates", func(txn *sql.Tx, conf *goose.DBConf) error {
addGivenNameColumn := "ALTER TABLE local_users ADD given_name VARCHAR(128);"
_, err := txn.Exec(addGivenNameColumn)
if err != nil {
return err
}

addFamilyNameColumn := "ALTER TABLE local_users ADD family_name VARCHAR(128);"
_, err = txn.Exec(addFamilyNameColumn)
if err != nil {
return err
}

// All existing data will not have values, so set to defaults
populate := "UPDATE local_users SET given_name='Admin', family_name='User'"
_, err = txn.Exec(populate)
if err != nil {
return err
}

return nil
})
}
152 changes: 152 additions & 0 deletions src/jetstream/plugins/userinfo/local_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package userinfo

import (
"encoding/json"
"net/http"

"golang.org/x/crypto/bcrypt"

"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/localusers"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces"
)

// LocalUserInfo is a plugin to fetch user info
type LocalUserInfo struct {
portalProxy interfaces.PortalProxy
}

// InitLocalUserInfo creates a new local user info provider
func InitLocalUserInfo(portalProxy interfaces.PortalProxy) Provider {
return &LocalUserInfo{portalProxy: portalProxy}
}

// GetUserInfo gets info for the specified user
func (userInfo *LocalUserInfo) GetUserInfo(id string) (int, []byte, error) {

localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(userInfo.portalProxy.GetDatabaseConnection())
if err != nil {
return 500, nil, err
}

user, err := localUsersRepo.FindUser(id)
if err != nil {
return 500, nil, err
}

uaaUser := &uaaUser{
ID: id,
Origin: "local",
Username: user.Username,
}

emails := make([]uaaUserEmail, 1)
emails[0] = uaaUserEmail{Value: user.Email}
uaaUser.Emails = emails

uaaUser.Name.GivenName = user.GivenName
uaaUser.Name.FamilyName = user.FamilyName

groups := make([]uaaUserGroup, 2)
groups[0] = uaaUserGroup{Display: user.Scope}
groups[1] = uaaUserGroup{Display: "password.write"}
uaaUser.Groups = groups

uaaUser.Meta.Version = 0

jsonString, err := json.Marshal(uaaUser)
if err != nil {
return 500, nil, err
}

return 200, jsonString, nil
}

// UpdateUserInfo updates the user's info
func (userInfo *LocalUserInfo) UpdateUserInfo(profile *uaaUser) (error) {

// Fetch the user, make updates and save
id := profile.ID
localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(userInfo.portalProxy.GetDatabaseConnection())
if err != nil {
return err
}

user, err := localUsersRepo.FindUser(id)
if err != nil {
return err
}

hash, err := localUsersRepo.FindPasswordHash(id)
if err != nil {
return err
}

user.PasswordHash = hash

if len(profile.Emails) == 1 {
email := profile.Emails[0]
if len(email.Value) >0 {
user.Email = email.Value
}
}

user.GivenName = profile.Name.GivenName
user.FamilyName = profile.Name.FamilyName

err = localUsersRepo.UpdateLocalUser(user)
if err != nil {
return err
}

return nil
}

// UpdatePassword updates the user's password
func (userInfo *LocalUserInfo) UpdatePassword(id string, passwordInfo *passwordChangeInfo) (error) {

// Fetch the user, make updates and save
localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(userInfo.portalProxy.GetDatabaseConnection())
if err != nil {
return err
}

user, err := localUsersRepo.FindUser(id)
if err != nil {
return err
}

hash, err := localUsersRepo.FindPasswordHash(id)
if err != nil {
return err
}

// Check old password is correct
err = bcrypt.CompareHashAndPassword(hash, []byte(passwordInfo.OldPassword))
if err != nil {
// Old password is incorrect
return interfaces.NewHTTPShadowError(
http.StatusBadRequest,
"Current password is incorrect",
"Current password is incorrect: %v", err,
)
}

passwordHash, err := HashPassword(passwordInfo.NewPassword)
if err != nil {
return err
}

user.PasswordHash = passwordHash

err = localUsersRepo.UpdateLocalUser(user)
if err != nil {
return err
}
return nil
}

//HashPassword accepts a plaintext password string and generates a salted hash
func HashPassword(password string) ([]byte, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return bytes, err
}
Loading

0 comments on commit 8058008

Please sign in to comment.