fix: user grants deactivation (#8634)

# Which Problems Are Solved

ZITADEL's user grants deactivation mechanism did not work correctly.
Deactivated user grants were still provided in token, which could lead
to unauthorized access to applications and resources.
Additionally, the management and auth API always returned the state as
active or did not provide any information about the state.

# How the Problems Are Solved

- Correctly check the user grant state on active for tokens and user
information (userinfo, introspection, saml attributes)
- Map state in API and display in Console
This commit is contained in:
Livio Spring 2024-09-17 14:18:29 +02:00 committed by GitHub
parent 4ac722d934
commit ca1914e235
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 95 additions and 12 deletions

View File

@ -154,10 +154,25 @@
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.GRANT.STATE' | translate }}</th>
<td mat-cell *matCellDef="let grant">
<span
class="state"
[ngClass]="{
active: grant.state === UserGrantState.USER_GRANT_STATE_ACTIVE,
inactive: grant.state === UserGrantState.USER_GRANT_STATE_INACTIVE,
}"
>
{{ 'USER.DATA.STATE' + grant.state | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
<td mat-cell class="user-tr-actions" *matCellDef="let grant; let i = index">
<cnsl-table-actions [hasActions]="true">
<cnsl-table-actions [hasActions]="!context.includes('user')">
<button
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"

View File

@ -8,7 +8,13 @@ import { tap } from 'rxjs/operators';
import { enterAnimations } from 'src/app/animations';
import { UserGrant as AuthUserGrant } from 'src/app/proto/generated/zitadel/auth_pb';
import { Role } from 'src/app/proto/generated/zitadel/project_pb';
import { Type, UserGrant as MgmtUserGrant, UserGrantQuery, UserGrant } from 'src/app/proto/generated/zitadel/user_pb';
import {
Type,
UserGrant as MgmtUserGrant,
UserGrant,
UserGrantQuery,
UserGrantState,
} from 'src/app/proto/generated/zitadel/user_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@ -66,6 +72,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public UserGrantContext: any = UserGrantContext;
public Type: any = Type;
public ActionKeysType: any = ActionKeysType;
public UserGrantState: any = UserGrantState;
@Input() public type: Type | undefined = undefined;
public filterOpen: boolean = false;
@ -86,6 +93,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
'type',
'creationDate',
'changeDate',
'state',
'roleNamesList',
'actions',
];

View File

@ -17,6 +17,7 @@
'type',
'creationDate',
'changeDate',
'state',
'roleNamesList',
'actions',
]"

View File

@ -42,7 +42,16 @@
[context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId"
[grantId]="grantId"
[displayedColumns]="['select', 'user', 'projectId', 'creationDate', 'changeDate', 'roleNamesList', 'actions']"
[displayedColumns]="[
'select',
'user',
'projectId',
'creationDate',
'changeDate',
'state',
'roleNamesList',
'actions',
]"
[disableWrite]="(['user.grant.write$', 'user.grant.write:' + grantId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + grantId] | hasRole | async) === false"
[refreshOnPreviousRoutes]="['/grant-create/project/{{projectId}}/grant/{{grantId}}']"

View File

@ -187,7 +187,7 @@
<cnsl-user-grants
[context]="UserGrantContext.OWNED_PROJECT"
[projectId]="projectId"
[displayedColumns]="['select', 'user', 'creationDate', 'changeDate', 'roleNamesList', 'actions']"
[displayedColumns]="['select', 'user', 'creationDate', 'changeDate', 'state', 'roleNamesList', 'actions']"
[refreshOnPreviousRoutes]="['/grant-create/project/' + projectId]"
[disableWrite]="(['user.grant.write$', 'user.grant.write:' + projectId] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$', 'user.grant.delete:' + projectId] | hasRole | async) === false"

View File

@ -136,7 +136,16 @@
<cnsl-user-grants
[userId]="user.id"
[context]="USERGRANTCONTEXT"
[displayedColumns]="['org', 'projectId', 'type', 'creationDate', 'changeDate', 'roleNamesList', 'actions']"
[displayedColumns]="[
'org',
'projectId',
'type',
'creationDate',
'changeDate',
'state',
'roleNamesList',
'actions',
]"
[disableWrite]="(['user.grant.write$'] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$'] | hasRole | async) === false"
>

View File

@ -222,7 +222,7 @@
<cnsl-user-grants
[userId]="user.id"
[context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'roleNamesList', 'actions']"
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'state', 'roleNamesList', 'actions']"
[disableWrite]="(['user.grant.write$'] | hasRole | async) === false"
[disableDelete]="(['user.grant.delete$'] | hasRole | async) === false"
>

View File

@ -55,5 +55,6 @@ func UserGrantToPb(grant *query.UserGrant) *auth_pb.UserGrant {
ProjectGrantId: grant.GrantID,
RoleKeys: grant.Roles,
UserType: user.TypeToPb(grant.UserType),
State: user.UserGrantStateToPb(grant.State),
}
}

View File

@ -23,7 +23,7 @@ func UserGrantToPb(assetPrefix string, grant *query.UserGrant) *user_pb.UserGran
return &user_pb.UserGrant{
Id: grant.ID,
UserId: grant.UserID,
State: user_pb.UserGrantState_USER_GRANT_STATE_ACTIVE,
State: UserGrantStateToPb(grant.State),
RoleKeys: grant.Roles,
ProjectId: grant.ProjectID,
OrgId: grant.ResourceOwner,
@ -51,6 +51,21 @@ func UserGrantToPb(assetPrefix string, grant *query.UserGrant) *user_pb.UserGran
}
}
func UserGrantStateToPb(state domain.UserGrantState) user_pb.UserGrantState {
switch state {
case domain.UserGrantStateActive:
return user_pb.UserGrantState_USER_GRANT_STATE_ACTIVE
case domain.UserGrantStateInactive:
return user_pb.UserGrantState_USER_GRANT_STATE_INACTIVE
case domain.UserGrantStateRemoved,
domain.UserGrantStateUnspecified:
// these states should never occur here and are mainly listed for linting purposes
fallthrough
default:
return user_pb.UserGrantState_USER_GRANT_STATE_UNSPECIFIED
}
}
func UserGrantQueriesToQuery(ctx context.Context, queries []*user_pb.UserGrantQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {

View File

@ -799,19 +799,24 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin
if projectID != "" {
roleAudience = append(roleAudience, projectID)
}
queries := make([]query.SearchQuery, 0, 2)
projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience)
if err != nil {
return nil, nil, err
}
queries = append(queries, projectQuery)
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
if err != nil {
return nil, nil, err
}
queries = append(queries, userIDQuery)
activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive)
if err != nil {
return nil, nil, err
}
grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: queries,
Queries: []query.SearchQuery{
projectQuery,
userIDQuery,
activeQuery,
},
}, true)
if err != nil {
return nil, nil, err

View File

@ -324,10 +324,15 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) (
if err != nil {
return nil, err
}
activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive)
if err != nil {
return nil, err
}
return p.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{
projectQuery,
userIDQuery,
activeQuery,
},
}, true)
}

View File

@ -11,6 +11,7 @@ import (
sd "github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
eventstore2 "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/query"
@ -119,7 +120,11 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj
if err != nil {
return nil, err
}
queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID}}
activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive)
if err != nil {
return nil, err
}
queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID, activeQuery}}
grants, err := q.Queries.UserGrants(ctx, queries, true)
if err != nil {
return nil, err

View File

@ -143,6 +143,10 @@ func NewUserGrantRoleQuery(value string) (SearchQuery, error) {
return NewTextQuery(UserGrantRoles, value, TextListContains)
}
func NewUserGrantStateQuery(value domain.UserGrantState) (SearchQuery, error) {
return NewNumberQuery(UserGrantState, value, NumberEquals)
}
func NewUserGrantWithGrantedQuery(owner string) (SearchQuery, error) {
orgQuery, err := NewUserGrantResourceOwnerSearchQuery(owner)
if err != nil {

View File

@ -38,6 +38,7 @@ user_grants as (
where user_id = $1
and instance_id = $2
and project_id = any($3)
and state = 1
{{ if . -}}
and resource_owner = any($4)
{{- end }}

View File

@ -1565,6 +1565,11 @@ message UserGrant {
description: "type of the user (human / machine)"
}
];
zitadel.user.v1.UserGrantState state = 13 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "current state of the user grant";
}
];
}
message ListMyProjectOrgsRequest {