Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin signoffs for changes to permissions #3088

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b64522a
Set required signoffs for permission changes to two admin roles
michellemounde Jan 16, 2024
3894465
Set serialization of signoffs to check for permission if role is not …
michellemounde Jan 16, 2024
7ecee78
Modify signoff summary to account for permission check instead of rol…
michellemounde Jan 16, 2024
0b1c86b
Enable auto-signoff when signoff based on permission
michellemounde Jan 16, 2024
ccdb48c
Frontend lint fixes
michellemounde Jan 16, 2024
3f43d29
Backend lint fixes
michellemounde Jan 16, 2024
18b6b7c
Update signoff requirements serialization to check for permission or …
michellemounde Jan 19, 2024
b1059c6
Fix auto-signoff fail when checking for user permission
michellemounde Jan 19, 2024
731fb37
Update permission auto_signoffs to check for all matching permissions…
michellemounde Jan 22, 2024
8e39fd5
Update tests for adding scheduled changes to check for user permission
michellemounde Jan 25, 2024
fb3c1e6
Update signoff verification to check for user permission
michellemounde Jan 25, 2024
a055adc
Update test for adding full admin permission to signoff with two admins
michellemounde Jan 25, 2024
29a0877
Update tests for permissions table that required signoffs
michellemounde Jan 26, 2024
485d7f5
Modify setup function for permission scheduled changes with permissio…
michellemounde Jan 26, 2024
e39dc2c
Update tests for getting ongoing and completed scheduled changes with…
michellemounde Jan 26, 2024
a6a52ce
Modify scheduled change update tests with permission based signoff check
michellemounde Jan 26, 2024
05403a3
Mock required signoffs to empty dict for permissions API tests
michellemounde Jan 29, 2024
60d4535
Update required signoffs for completed scheduled changes
michellemounde Jan 29, 2024
396a091
Modify signoff length for revoked signoff test to match new permissio…
michellemounde Jan 29, 2024
8d9466a
Reverts 60d45355
michellemounde Jan 29, 2024
7b7b103
Lint fixes
michellemounde Jan 30, 2024
cf8ef16
Modify test for existing permission different user resetting signoffs…
michellemounde Jan 30, 2024
d7f508d
Add new scheduled change for testing permission based signoff
michellemounde Jan 30, 2024
7d20afc
Update test functions returning all scheduled changes with new schedu…
michellemounde Jan 30, 2024
7d976ba
Update scheduled change id not in setup from 7 to 8
michellemounde Jan 30, 2024
091b306
Modify signoff without permission to check signoff by admin with rest…
michellemounde Jan 31, 2024
e0e60c2
Update signoff without role to use new test scheduled change
michellemounde Jan 31, 2024
acc8f7c
Add test to check that permission signoffs check permission not role
michellemounde Jan 31, 2024
26b7443
Readabilty fix to match the rest of the setup function
michellemounde Jan 31, 2024
2808617
Fix expected result of getting all scheduled changes to match functio…
michellemounde Jan 31, 2024
b6b339d
Create helper function to remove signoffs for permission api tests th…
michellemounde Jan 31, 2024
37ce5b3
Lint fixes
michellemounde Jan 31, 2024
9d23cb8
Create env variable for admin required signoffs
michellemounde Feb 1, 2024
603ea2c
Set up permission admin required signoffs for tests
michellemounde Feb 1, 2024
9acb64f
Remove permission required signoffs list & ability to create new perm…
michellemounde Feb 1, 2024
3c0f3a6
Check for None user_permissions in auto-signoff & revert signoffs ins…
michellemounde Feb 26, 2024
9ca56b3
Set staging required signoffs to 1
michellemounde Feb 28, 2024
db6d210
Refactor signoffs insert error checks to improve readability
michellemounde Feb 28, 2024
2369aa1
Pass signoff_verifier to verify_signoffs
michellemounde Feb 29, 2024
ef4f83a
Lint fixes
michellemounde Feb 29, 2024
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
110 changes: 74 additions & 36 deletions src/auslib/db.py

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/auslib/util/signoffs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
def serialize_signoff_requirements(requirements):
dct = {}
for rs in requirements:
signoffs_required = max(dct.get(rs["role"], 0), rs["signoffs_required"])
dct[rs["role"]] = signoffs_required
signoff_verifier = "role" if "role" in rs else "permission"
signoffs_required = max(dct.get(rs[signoff_verifier], 0), rs["signoffs_required"])
dct[rs[signoff_verifier]] = signoffs_required

return dct
1 change: 1 addition & 0 deletions tests/admin/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def my_userinfo(*args, **kwargs):
dbo.setDomainAllowlist({"good.com": ("a", "b", "c", "d")})
self.metadata.create_all(dbo.engine)
dbo.permissions.t.insert().execute(permission="admin", username="bill", data_version=1)
dbo.permissions.t.insert().execute(permission="admin", username="zawadi", data_version=1)
dbo.permissions.t.insert().execute(permission="permission", username="bob", data_version=1)
dbo.permissions.t.insert().execute(
permission="release", username="bob", options=dict(products=["fake", "a", "b"], actions=["create", "modify"]), data_version=1
Expand Down
256 changes: 179 additions & 77 deletions tests/admin/views/test_permissions.py

Large diffs are not rendered by default.

48 changes: 36 additions & 12 deletions tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,30 +83,34 @@ def getTempfile(self):

class TestVerifySignoffs(unittest.TestCase):
def testNoRequiredSignoffs(self):
verify_signoffs({}, {})
verify_signoffs({}, {}, "role")

def testNoRequiredSignoffsWithSignoffs(self):
verify_signoffs({}, [{"role": "releng"}, {"role": "relman"}])
verify_signoffs({}, [{"role": "releng"}, {"role": "relman"}], "role")

def testNoSignoffsGiven(self):
required = [{"role": "releng", "signoffs_required": 1}]
signoffs = []
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs)
signoff_verifier = "role"
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs, signoff_verifier)

def testMissingSignoffFromOneRole(self):
required = [{"role": "releng", "signoffs_required": 1}, {"role": "relman", "signoffs_required": 1}]
signoffs = [{"role": "releng", "username": "joe"}]
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs)
signoff_verifier = "role"
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs, signoff_verifier)

def testNotEnoughSignoffsFromOneRole(self):
required = [{"role": "releng", "signoffs_required": 2}, {"role": "relman", "signoffs_required": 1}]
signoffs = [{"role": "releng", "username": "joe"}, {"role": "relman", "username": "jane"}]
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs)
signoff_verifier = "role"
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs, signoff_verifier)

def testExactlyEnoughSignoffsGiven(self):
required = [{"role": "releng", "signoffs_required": 2}, {"role": "relman", "signoffs_required": 1}]
signoffs = [{"role": "releng", "username": "joe"}, {"role": "releng", "username": "jane"}, {"role": "relman", "username": "nick"}]
verify_signoffs(required, signoffs)
signoff_verifier = "role"
verify_signoffs(required, signoffs, signoff_verifier)

def testMoreThanEnoughSignoffsGiven(self):
required = [{"role": "releng", "signoffs_required": 2}, {"role": "relman", "signoffs_required": 1}]
Expand All @@ -116,17 +120,20 @@ def testMoreThanEnoughSignoffsGiven(self):
{"role": "relman", "username": "nick"},
{"role": "relman", "username": "matt"},
]
verify_signoffs(required, signoffs)
signoff_verifier = "role"
verify_signoffs(required, signoffs, signoff_verifier)

def testMultiplePotentialSignoffsForOneGroupWithoutEnoughSignoffs(self):
required = [{"role": "releng", "signoffs_required": 2}, {"role": "releng", "signoffs_required": 1}, {"role": "relman", "signoffs_required": 1}]
signoffs = [{"role": "releng", "username": "joe"}, {"role": "relman", "username": "nick"}]
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs)
signoff_verifier = "role"
self.assertRaises(SignoffRequiredError, verify_signoffs, required, signoffs, signoff_verifier)

def testMultiplePotentialSignoffsForOneGroupWithEnoughSignoffs(self):
required = [{"role": "releng", "signoffs_required": 2}, {"role": "releng", "signoffs_required": 1}, {"role": "relman", "signoffs_required": 1}]
signoffs = [{"role": "releng", "username": "joe"}, {"role": "releng", "username": "jane"}, {"role": "relman", "username": "nick"}]
verify_signoffs(required, signoffs)
signoff_verifier = "role"
verify_signoffs(required, signoffs, signoff_verifier)


class TestAUSTransaction(unittest.TestCase, MemoryDatabaseMixin):
Expand Down Expand Up @@ -5321,6 +5328,7 @@ def setUp(self):
self.metadata.create_all(self.db.engine)
self.permissions = self.db.permissions
self.user_roles = self.db.permissions.user_roles
self.db.setAdminRequiredSignoffs([{"permission": "admin", "signoffs_required": 2}])
self.permissions.t.insert().execute(permission="admin", username="bill", data_version=1)
self.permissions.t.insert().execute(permission="permission", username="bob", data_version=1)
self.permissions.t.insert().execute(permission="permission", username="sean", data_version=1)
Expand Down Expand Up @@ -5362,7 +5370,13 @@ def testUserRolesHasCorrectTablesAndColumns(self):
self.assertEqual(set(history_columns), set(expected))

def testGrantPermissions(self):
self.permissions.insert("bob", username="cathy", permission="release", options=dict(products=["SeaMonkey"]))
self.permissions.insert(
"bob",
signoffs=[{"sc_id": 1, "username": "bill", "role": "admin"}, {"sc_id": 1, "username": "zawadi", "role": "admin"}],
username="cathy",
permission="release",
options=dict(products=["SeaMonkey"]),
)
query = self.permissions.t.select().where(self.permissions.username == "cathy")
query = query.where(self.permissions.permission == "release")
self.assertEqual(query.execute().fetchall(), [("release", "cathy", dict(products=["SeaMonkey"]), 1)])
Expand Down Expand Up @@ -5405,7 +5419,12 @@ def testGrantRoleToUserWhoDoesntHaveAPermission(self):
)

def testRevokePermission(self):
self.permissions.delete({"username": "bob", "permission": "release"}, changed_by="bill", old_data_version=1)
self.permissions.delete(
{"username": "bob", "permission": "release"},
changed_by="bill",
old_data_version=1,
signoffs=[{"sc_id": 5, "username": "bill", "role": "admin"}, {"sc_id": 5, "username": "zawadi", "role": "admin"}],
)
query = self.permissions.t.select().where(self.permissions.username == "bob")
query = query.where(self.permissions.permission == "release")
self.assertEqual(len(query.execute().fetchall()), 0)
Expand All @@ -5425,7 +5444,12 @@ def testRevokeRoleWithoutPermission(self):
self.assertRaises(PermissionDeniedError, self.permissions.revokeRole, username="bob", role="releng", changed_by="kirk", old_data_version=1)

def testRevokingPermissionAlsoRevokeRoles(self):
self.permissions.delete({"username": "janet", "permission": "release"}, changed_by="bill", old_data_version=1)
self.permissions.delete(
{"username": "janet", "permission": "release"},
changed_by="bill",
old_data_version=1,
signoffs=[{"sc_id": 3, "username": "bill", "role": "admin"}, {"sc_id": 3, "username": "zawadi", "role": "admin"}],
)
got = self.db.permissions.t.select().where(self.db.permissions.username == "janet").execute().fetchall()
self.assertEqual(len(got), 0)
got = self.user_roles.t.select().where(self.user_roles.username == "janet").execute().fetchall()
Expand Down
8 changes: 7 additions & 1 deletion ui/src/components/SignoffSummary/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ function SignoffSummary(props) {
<ListItemText
primary={
<Typography component="p" variant="body2">
{`${count} member${count > 1 ? 's' : ''} of ${role} -`}
{role === 'admin'
? `${count} member${
count > 1 ? 's' : ''
} with full fledged ${role} permissions - `
: `${count} member${
count > 1 ? 's' : ''
} of ${role} - `}
</Typography>
}
className={classes.listItemText}
Expand Down
84 changes: 8 additions & 76 deletions ui/src/views/RequiredSignoffs/ListSignoffs/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
} from '../../../utils/constants';
import { withUser } from '../../../utils/AuthContext';

const getPermissionChangesLens = product => lensPath([product, 'permissions']);
const getRulesOrReleasesChangesLens = product =>
lensPath([product, 'channels']);
const useStyles = makeStyles(theme => ({
Expand Down Expand Up @@ -101,10 +100,6 @@ function ListSignoffs({ user, ...props }) {

const handleSignoffRoleChange = ({ target: { value } }) =>
setSignoffRole(value);
const permissionChanges = view(
getPermissionChangesLens(product),
requiredSignoffs
);
const rulesOrReleasesChanges = view(
getRulesOrReleasesChangesLens(product),
requiredSignoffs
Expand Down Expand Up @@ -269,16 +264,6 @@ function ListSignoffs({ user, ...props }) {
{requiredSignoffs && (
<Fragment>
<div className={classes.toolbar}>
{permissionChanges && (
<Typography gutterBottom variant="h5">
Changes to Permissions
</Typography>
)}
{!permissionChanges && rulesOrReleasesChanges && (
<Typography gutterBottom variant="h5">
Changes to Rules / Releases
</Typography>
)}
<div className={classes.dropdownDiv}>
<TextField
className={classes.dropdown}
Expand All @@ -294,69 +279,16 @@ function ListSignoffs({ user, ...props }) {
</TextField>
</div>
</div>
{permissionChanges && (
<SignoffCard
className={classes.card}
title={capitalCase(product)}
to={`/required-signoffs/${product}`}>
{Object.entries(permissionChanges).map(
([name, role], index, arr) => {
const key = `${name}-${index}`;

return (
<Fragment key={key}>
<SignoffCardEntry
key={name}
name={name}
entry={role}
onCancelDelete={() =>
handleCancelDelete(
OBJECT_NAMES.PERMISSIONS_REQUIRED_SIGNOFF,
role,
name,
product
)
}
onSignoff={() =>
handleSignoff(
OBJECT_NAMES.PERMISSIONS_REQUIRED_SIGNOFF,
role,
name,
product
)
}
onRevoke={() =>
handleRevoke(
OBJECT_NAMES.PERMISSIONS_REQUIRED_SIGNOFF,
role,
name,
product
)
}
/>
<Divider
className={classNames({
[classes.lastDivider]: arr.length - 1 === index,
})}
/>
</Fragment>
);
}
)}
</SignoffCard>
)}
<div>
{rulesOrReleasesChanges && (
<Fragment>
{permissionChanges && (
<Fragment>
<br />
<Typography gutterBottom variant="h5">
Changes to Rules / Releases
</Typography>
<br />
</Fragment>
)}
<Fragment>
<br />
<Typography gutterBottom variant="h5">
Changes to Rules / Releases
</Typography>
<br />
</Fragment>
{Object.entries(rulesOrReleasesChanges).map(
([channelName, roles]) => (
<SignoffCard
Expand Down Expand Up @@ -417,7 +349,7 @@ function ListSignoffs({ user, ...props }) {
)}
</Fragment>
)}
{!permissionChanges && !rulesOrReleasesChanges && (
{!rulesOrReleasesChanges && (
<Typography>No required signoffs for {product}</Typography>
)}
</div>
Expand Down
6 changes: 0 additions & 6 deletions ui/src/views/RequiredSignoffs/ViewSignoff/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,6 @@ function ViewSignoff({ isNewSignoff, ...props }) {
control={<Radio />}
label="Channel"
/>
<FormControlLabel
disabled={!isNewSignoff}
value="permission"
control={<Radio />}
label="Permission"
/>
</RadioGroup>
</FormControl>
</Grid>
Expand Down
5 changes: 5 additions & 0 deletions uwsgi/admin.wsgi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ if STAGING or LOCALDEV:
}
)

if STAGING or LOCALDEV:
ADMIN_REQUIRED_SIGNOFFS = [{"permission": "admin", "signoffs_required": 1}]
else:
ADMIN_REQUIRED_SIGNOFFS = [{"permission": "admin", "signoffs_required": 2}]

# Logging needs to be set-up before importing the application to make sure that
# logging done from other modules uses our Logger.
Expand Down Expand Up @@ -148,6 +152,7 @@ else:
dbo.setDb(os.environ["DBURI"], buckets)
dbo.setSystemAccounts(SYSTEM_ACCOUNTS)
dbo.setDomainAllowlist(DOMAIN_ALLOWLIST)
dbo.setAdminRequiredSignoffs(ADMIN_REQUIRED_SIGNOFFS)
application.config["ALLOWLISTED_DOMAINS"] = DOMAIN_ALLOWLIST
application.config["PAGE_TITLE"] = "Balrog Administration"
application.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
Expand Down