Skip to content

Commit

Permalink
initial API implementation of admin elevation request and expiration (#…
Browse files Browse the repository at this point in the history
…56)

* initial implementation of admin elevation request and expiration (to be handled by core addon)

* add wildcard requets subject for clients to use

* admin expiry permission check, new audit actions

* fix lint

* promotion terminology, no wildcard in event subject

* revert to one event subject
  • Loading branch information
jacobsee authored Sep 19, 2023
1 parent c26c336 commit aaca8c0
Show file tree
Hide file tree
Showing 11 changed files with 500 additions and 248 deletions.
15 changes: 15 additions & 0 deletions db/migrations/00033_group_request_types.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- +goose Up
-- +goose StatementBegin
CREATE TYPE request_kind AS ENUM ('new_member', 'admin_promotion');
ALTER TABLE group_membership_requests ADD COLUMN IF NOT EXISTS kind request_kind NOT NULL DEFAULT 'new_member';
ALTER TABLE group_memberships ADD COLUMN IF NOT EXISTS admin_expires_at TIMESTAMPTZ NULL;
ALTER TABLE group_membership_requests ADD COLUMN IF NOT EXISTS admin_expires_at TIMESTAMPTZ NULL;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE group_membership_requests DROP COLUMN IF EXISTS admin_expires_at;
ALTER TABLE group_memberships DROP COLUMN IF EXISTS admin_expires_at;
ALTER TABLE group_membership_requests DROP COLUMN kind;
DROP TYPE request_kind;
-- +goose StatementEnd
6 changes: 6 additions & 0 deletions internal/dbtools/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dbtools

import "errors"

// ErrUnknownRequestKind is returned a request kind is unknown
var ErrUnknownRequestKind = errors.New("request kind is unrecognized")
56 changes: 50 additions & 6 deletions internal/dbtools/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,20 +392,31 @@ func AuditGroupMemberPromoted(ctx context.Context, exec boil.ContextExecutor, pI
}

// AuditGroupMembershipApproved inserts an event representing group membership approval into the events table
func AuditGroupMembershipApproved(ctx context.Context, exec boil.ContextExecutor, pID string, actor *models.User, m *models.GroupMembership) ([]*models.AuditEvent, error) {
func AuditGroupMembershipApproved(ctx context.Context, exec boil.ContextExecutor, pID string, actor *models.User, m *models.GroupMembership, kind string) ([]*models.AuditEvent, error) {
// TODO non-user API actors don't exist in the governor database,
// we need to figure out how to handle that relationship in the audit table
var actorID null.String
if actor != nil {
actorID = null.StringFrom(actor.ID)
}

var action string

switch kind {
case "new_member":
action = "group.member.request.approved"
case "admin_promotion":
action = "admin.promotion.request.approved"
default:
return nil, ErrUnknownRequestKind
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(m.GroupID),
SubjectUserID: null.StringFrom(m.UserID),
Action: "group.member.request.approved",
Action: action,
Changeset: calculateGroupMembershipChangeset(&models.GroupMembership{}, m),
Message: "Request was approved.",
}
Expand All @@ -431,12 +442,23 @@ func AuditGroupMembershipRevoked(ctx context.Context, exec boil.ContextExecutor,
actorID = null.StringFrom(actor.ID)
}

var action string

switch r.Kind {
case "new_member":
action = "group.member.request.revoked"
case "admin_promotion":
action = "admin.promotion.request.revoked"
default:
return nil, ErrUnknownRequestKind
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(r.GroupID),
SubjectUserID: null.StringFrom(r.UserID),
Action: "group.member.request.revoked",
Action: action,
Changeset: []string{},
Message: "Request was revoked.",
}
Expand All @@ -453,12 +475,23 @@ func AuditGroupMembershipDenied(ctx context.Context, exec boil.ContextExecutor,
actorID = null.StringFrom(actor.ID)
}

var action string

switch r.Kind {
case "new_member":
action = "group.member.request.denied"
case "admin_promotion":
action = "admin.promotion.request.denied"
default:
return nil, ErrUnknownRequestKind
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(r.GroupID),
SubjectUserID: null.StringFrom(r.UserID),
Action: "group.member.request.denied",
Action: action,
Changeset: []string{},
Message: "Request was denied.",
}
Expand All @@ -475,14 +508,25 @@ func AuditGroupMembershipRequestCreated(ctx context.Context, exec boil.ContextEx
actorID = null.StringFrom(actor.ID)
}

var action string

switch r.Kind {
case "new_member":
action = "group.member.request.created"
case "admin_promotion":
action = "admin.promotion.request.created"
default:
return nil, ErrUnknownRequestKind
}

event := models.AuditEvent{
ParentID: null.StringFrom(pID),
ActorID: actorID,
SubjectGroupID: null.StringFrom(r.GroupID),
SubjectUserID: null.StringFrom(r.UserID),
Action: "group.member.request.created",
Action: action,
Changeset: []string{},
Message: "User requested to join group.",
Message: "Request was created.",
}

return &event, event.Insert(ctx, exec, boil.Infer())
Expand Down
34 changes: 27 additions & 7 deletions internal/dbtools/membership_enumeration.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
group_id,
user_id,
expires_at,
admin_expires_at,
is_admin,
TRUE AS direct
FROM
Expand All @@ -50,6 +51,7 @@ const (
b.parent_group_id,
a.user_id,
b.expires_at,
b.admin_expires_at,
FALSE AS is_admin,
FALSE AS direct
FROM
Expand All @@ -64,6 +66,11 @@ const (
ELSE
NULL
END AS expires_at,
CASE WHEN BOOL_OR(direct) THEN
MAX(admin_expires_at)
ELSE
NULL
END AS admin_expires_at,
BOOL_OR(is_admin) as is_admin,
BOOL_OR(direct) as direct
FROM
Expand All @@ -77,6 +84,7 @@ const (
user_id,
is_admin,
expires_at,
admin_expires_at,
TRUE AS direct
FROM
group_memberships
Expand All @@ -87,6 +95,7 @@ const (
a.user_id,
FALSE AS is_admin,
NULL as expires_at,
NULL as admin_expires_at,
FALSE AS direct
FROM
membership_query AS a
Expand All @@ -100,6 +109,11 @@ const (
ELSE
NULL
END AS expires_at,
CASE WHEN BOOL_OR(direct) THEN
MAX(admin_expires_at)
ELSE
NULL
END AS admin_expires_at,
BOOL_OR(is_admin) as is_admin,
BOOL_OR(direct) as direct
FROM
Expand Down Expand Up @@ -149,6 +163,11 @@ const (
ELSE
NULL
END AS expires_at,
CASE WHEN BOOL_OR(direct) THEN
MAX(group_memberships.admin_expires_at)
ELSE
NULL
END AS admin_expires_at,
BOOL_OR(direct) as direct
FROM
ensure_root
Expand All @@ -159,13 +178,14 @@ const (

// EnumeratedMembership represents a single user-to-group membership, which may be direct or indirect
type EnumeratedMembership struct {
GroupID string
Group *models.Group
UserID string
User *models.User
IsAdmin bool
ExpiresAt null.Time
Direct bool
GroupID string
Group *models.Group
UserID string
User *models.User
IsAdmin bool
ExpiresAt null.Time
AdminExpiresAt null.Time
Direct bool
}

// GetMembershipsForUser returns a fully enumerated list of memberships for a user, optionally with sqlboiler's generated models populated
Expand Down
130 changes: 72 additions & 58 deletions internal/models/group_membership_requests.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit aaca8c0

Please sign in to comment.