Skip to content
This repository has been archived by the owner on Jan 24, 2023. It is now read-only.

Add support for view and edit profile for local user #3883

Merged
merged 3 commits into from
Sep 23, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -12,7 +12,7 @@ <h1>User Profile</h1>
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 +24,17 @@ <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 @@ -77,12 +83,12 @@ <h1>User Profile</h1>
[color]="pollingEnabled === 'true' ? 'warn' : 'primary'">
{{ pollingEnabled === 'true' ? 'Disable' : 'Enable' }} polling
</button>
<div class="user-profile__option-subtext">Disabling polling wil result in some pages not automatically
<div class="user-profile__option-subtext">Disabling polling will result in some views not automatically
updating.</div>
</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<AppState>,
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 @@ -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 @@ -49,7 +49,7 @@ export class UserProfileEffect {
type: action.type,
} as IRequestAction;
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 @@ -82,7 +82,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 @@ -114,7 +114,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